I thought it might be useful to have a thread that contains example code for commonly needed mod features. Eventually I hope to have a long list of working code samples for modders to play around with.
First up, I have two examples of how to use SpawnPointPlugin to create a mod control object. The first example runs events at timed intervals (each day, week, month, and year):
EventManager.javapackage data.scripts.world;
import com.fs.starfarer.api.campaign.LocationAPI;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.fs.starfarer.api.campaign.SpawnPointPlugin;
import java.util.GregorianCalendar;
public class EventManager implements SpawnPointPlugin
{
private static final float BASE_INTERVAL = 1.0f;
private static final int FIRST_DAY_IN_WEEK = GregorianCalendar.SUNDAY;
private float heartbeatInterval;
private long lastHeartbeat;
private SectorAPI sector;
private GregorianCalendar calendar = new GregorianCalendar();
public EventManager(SectorAPI sector)
{
// Synch the heartbeat to the sector clock
this.sector = sector;
lastHeartbeat = sector.getClock().getTimestamp();
// The first heartbeat should happen at the start of day 1
heartbeatInterval = (1.0f
- (sector.getClock().getHour() / 24f));
}
private void runDaily()
{
sector.addMessage("Daily");
// Insert code here
}
private void runWeekly()
{
sector.addMessage("Weekly");
// Insert code here
}
private void runMonthly()
{
sector.addMessage("Monthly");
// Insert code here
}
private void runYearly()
{
sector.addMessage("Yearly");
// Insert code here
}
private void doIntervalChecks(long time)
{
lastHeartbeat = time;
runDaily();
calendar.setTimeInMillis(time);
if (calendar.get(GregorianCalendar.DAY_OF_WEEK) == FIRST_DAY_IN_WEEK)
{
runWeekly();
}
if (calendar.get(GregorianCalendar.DAY_OF_MONTH) == 1)
{
runMonthly();
if (calendar.get(GregorianCalendar.DAY_OF_YEAR) == 1)
{
runYearly();
}
}
}
private void checkSynched()
{
// Compensate for day-synch code in constructor
if (heartbeatInterval != BASE_INTERVAL)
{
heartbeatInterval = BASE_INTERVAL;
}
}
@Override
public void advance(SectorAPI sector, LocationAPI location)
{
// Events that run at set in-game intervals
if (sector.getClock().getElapsedDaysSince(lastHeartbeat) >= heartbeatInterval)
{
doIntervalChecks(sector.getClock().getTimestamp());
checkSynched();
}
}
}
The second example contains two classes, and will allow you to set scripts to run at any time you wish (either by timestamp, calendar date, or a set time from now):
TimedScriptManager.javapackage data.scripts.world;
import com.fs.starfarer.api.Script;
import com.fs.starfarer.api.campaign.LocationAPI;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.fs.starfarer.api.campaign.SpawnPointPlugin;
import java.util.*;
public class TimedScriptManager implements SpawnPointPlugin
{
// If an added script has already passed its target runtime, should it run?
private static final boolean RUN_SCRIPTS_IF_ADDED_LATE = false;
private static final boolean SHOW_DEBUG_MESSAGES = false;
// Helps convert between Starfarer and RL time
private static final long STARFARER_SPEED = 8640000l;
// Holds all TimedScriptManagers for easy lookup and (theoretical)
// multi-sector support; get current Manager with getManager(SectorAPI)
private static Map allManagers = Collections.synchronizedMap(new HashMap());
// The sector the current object manages
private SectorAPI sector;
// This stores our scripts in a sorted set for efficient per-frame checks.
// Sets don't allow duplicates, so the scripts are stored in a container
// object that does (if there are two containers with the same timestamp,
// mergeScripts will be called to add one container's scripts to the other)
private SortedSet runOnceScripts = new TreeSet();
public TimedScriptManager(SectorAPI sector)
{
// Don't add this to the list of managers if the sector has one already
if (allManagers.keySet().contains(sector))
{
sector.addMessage("Duplicate TimedScriptManager detected!");
return;
}
this.sector = sector;
allManagers.put(sector, this);
}
// Returns the manager associated with a sector
public static TimedScriptManager getManager(SectorAPI sector)
{
// If we have this sector registered, find the associated manager
if (allManagers.keySet().contains(sector))
{
return (TimedScriptManager) allManagers.get(sector);
}
// No such TimedScriptManager
return null;
}
// Runs the scripts at the provided timestamp
public void addScriptsAtTimestamp(long timestamp, Script[] scripts)
{
// If the target time has already passed, run the scripts instantly
if (timestamp <= sector.getClock().getTimestamp())
{
// .. unless you don't want to, that is
if (!RUN_SCRIPTS_IF_ADDED_LATE)
{
showDebug("Scripts skipped due to lateness.");
return;
}
showDebug("Running scripts early.");
// Run the scripts
for (int x = 0; x < scripts.length; x++)
{
scripts[x].run();
}
return;
}
// Create a container for the scripts (lazy optimization)
TimedScriptContainer tmp = new TimedScriptContainer(timestamp);
tmp.addScripts(scripts);
// Is there a container with this target time already?
if (runOnceScripts.contains(tmp))
{
showDebug("Debug: Two script containers with same timestamp!");
// Sets don't allow duplicates, so merge the two containers
mergeScripts(tmp);
return;
}
runOnceScripts.add(tmp);
}
// Same as above, but with a single script
public void addScriptAtTimestamp(long timestamp, Script script)
{
addScriptsAtTimestamp(timestamp, new Script[]
{
script
});
}
// Runs the scripts at the provided in-game date
public void addScriptsAtDate(GregorianCalendar date, Script[] scripts)
{
addScriptsAtTimestamp(date.getTimeInMillis(), scripts);
}
// Same as above, but with a single script
public void addScriptAtDate(GregorianCalendar date, Script script)
{
addScriptsAtDate(date, new Script[]
{
script
});
}
// Runs the scripts timeFromNow seconds after this is called
public void addScripts(float timeFromNow, Script[] scripts)
{
// Adjust time to compensate for Starfarer game speed
long timeToAdd = sector.getClock().getTimestamp();
timeToAdd += (long) (timeFromNow * STARFARER_SPEED);
addScriptsAtTimestamp(timeToAdd, scripts);
}
// Same as above, but with a single script
public void addScript(float timeFromNow, Script script)
{
addScripts(timeFromNow, new Script[]
{
script
});
}
// Sets don't allow duplicate elements, so if a container already exists
// with that timestamp, we must manually merge the scripts
private void mergeScripts(TimedScriptContainer container)
{
Iterator iter = runOnceScripts.iterator();
TimedScriptContainer tmp;
// Find the container that matches this container's timestamp
while (iter.hasNext())
{
tmp = (TimedScriptContainer) iter.next();
// Found it! Now combine our scripts with it
if (tmp.equals(container))
{
Script[] scripts = container.getScriptsAsArray();
tmp.addScripts(scripts);
showDebug("Debug: Merge successful!");
return;
}
}
// No matching container found (shouldn't ever happen)
showDebug("Debug: Merge failed!");
}
// Runs once per frame while there is at least one script container
private void checkScripts(long time)
{
Iterator iter = runOnceScripts.iterator();
TimedScriptContainer tmp;
Script script;
// Iterate through the script containers
while (iter.hasNext())
{
tmp = (TimedScriptContainer) iter.next();
// As the list of scripts is sorted, we only need
// to check until we've exceeded the current time
if (tmp.targetTime > time)
{
// This is extremely spammy (1 line per frame)
// Only uncomment if you have a problem you need to track down
//debugMsg("Debug: Ignoring " + tmp.targetTime
// + " for " + (tmp.targetTime - time));
break;
}
showDebug("Running " + tmp.targetTime + " (currently " + time + ")");
// Run all scripts in the container
for (int x = 0; x < tmp.scripts.size(); x++)
{
script = (Script) tmp.scripts.get(x);
script.run();
}
showDebug("Removing script container.");
// Remove container after it's been run
iter.remove();
}
}
private void showDebug(String text)
{
if (SHOW_DEBUG_MESSAGES)
{
sector.addMessage(text);
}
}
@Override
public void advance(SectorAPI sector, LocationAPI location)
{
// Only bother with the check if there are scripts to be run!
if (!runOnceScripts.isEmpty())
{
checkScripts(sector.getClock().getTimestamp());
}
}
}
TimedScriptContainer.javapackage data.scripts.world;
import com.fs.starfarer.api.Script;
import java.util.*;
public class TimedScriptContainer implements Comparable
{
public final long targetTime;
protected ArrayList scripts;
public TimedScriptContainer(long targetTime)
{
this.targetTime = targetTime;
scripts = new ArrayList();
}
public void addScript(Script toAdd)
{
scripts.add(toAdd);
}
public void addScripts(Script[] toAdd)
{
scripts.addAll(Arrays.asList(toAdd));
}
// For now, there should be no way to modify scripts once set
// I might change this later, when I have more time to debug
public final List getScripts()
{
return Collections.unmodifiableList(scripts);
}
public final Script[] getScriptsAsArray()
{
return (Script[]) scripts.toArray(new Script[scripts.size()]);
}
// Prevents containers with duplicate targetTimes from being added to a set
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (obj == this)
{
return true;
}
if (!(obj instanceof TimedScriptContainer))
{
return false;
}
TimedScriptContainer tmp = (TimedScriptContainer) obj;
return (targetTime == tmp.targetTime);
}
// Used by the SortedSet in TimedScriptManager to sort the containers
public int compareTo(Object obj)
{
TimedScriptContainer tmp = (TimedScriptContainer) obj;
return ((Long) targetTime).compareTo((Long) tmp.targetTime);
}
}
Here's a zip containing a mod using these examples:
linkI'm also working on a mini-tutorial on advanced Script usage. That will probably come later, though. I have some Caelus stuff to work on.
While I'm cleaning up these scripts for general use, are there any other examples anyone wants me to include? If it's a code-related problem, I can probably post a prototype for you.
Inspiration: this post, which made me realize how non-obvious some of the solutions we take for granted are.