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.java
package 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.java
package 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.java
package 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: link (http://dl.dropbox.com/u/32722116/ScriptTest.zip)
I'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 (http://fractalsoftworks.com/forum/index.php?topic=3758.msg60476#msg60476), which made me realize how non-obvious some of the solutions we take for granted are.
Code
Test
Code
Test
Code
Test
Code
Test
Having it not in a spoiler seems to work, at least in preview.
Sorry this took so long, I spent half an hour trying to get addScriptAtDate working before I realized my test case was using the year 206 instead of 205. :-[
Here's what I have so far for the arbitrary script manager. I probably made it more complex than I should have, but hopefully it's still understandable. :)
I'll clean it up a bit more, then add it to the main post once I get the interval event manager working as well.
TimedScriptManager.java
package data.scripts.world;
import com.fs.starfarer.api.Global;
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 (theoretically)
// 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;
}
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 <= Global.getSector().getClock().getTimestamp())
{
// .. unless you don't want to, that is
if (!RUN_SCRIPTS_IF_ADDED_LATE)
{
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 = Global.getSector().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
});
}
//<editor-fold defaultstate="collapsed" desc="Private methods">
// 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 run containers
iter.remove();
}
}
//</editor-fold>
public void advance(SectorAPI sector, LocationAPI location)
{
// Only bother with the check if there are scripts to be run!
if (!runOnceScripts.isEmpty())
{
checkScripts(Global.getSector().getClock().getTimestamp());
}
}
private void showDebug(String text)
{
if (SHOW_DEBUG_MESSAGES)
{
Global.getSector().addMessage(text);
}
}
}
TimedScriptContainer.java
package 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()]);
}
@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);
}
public int compareTo(Object obj)
{
TimedScriptContainer tmp = (TimedScriptContainer) obj;
return ((Long) targetTime).compareTo((Long) tmp.targetTime);
}
}
The Corvus.java I used for testing:
package data.scripts.world.corvus;
import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.Script;
import com.fs.starfarer.api.campaign.SectorAPI;
import com.fs.starfarer.api.campaign.SectorGeneratorPlugin;
import com.fs.starfarer.api.campaign.StarSystemAPI;
import data.scripts.world.TimedScriptManager;
import java.util.*;
@SuppressWarnings("unchecked")
public class Corvus implements SectorGeneratorPlugin
{
private class MessageScript implements Script
{
String message;
public MessageScript(String message)
{
this.message = message;
}
public void run()
{
Global.getSector().addMessage(message);
}
}
public void generate(SectorAPI sector)
{
StarSystemAPI system = sector.getStarSystem("Corvus");
TimedScriptManager scriptManager = new TimedScriptManager(sector);
system.addSpawnPoint(scriptManager);
long time = sector.getClock().getTimestamp();
// Test adding scripts
scriptManager.addScriptAtTimestamp((time + 4000l),
new MessageScript("Script success!"));
// Test duplicate timestamp merging
scriptManager.addScriptAtTimestamp((time + 400000l),
new MessageScript("Merged script 1 success!"));
scriptManager.addScriptAtTimestamp((time + 400000l),
new MessageScript("Merged script 2 success!"));
// Test adding multiple scripts at once
scriptManager.addScriptsAtTimestamp((time + 99999999),
new Script[]
{
new MessageScript("Script 1 success!"),
new MessageScript("Script 2 success!"),
new MessageScript("Script 3 success!")
});
// Test adding scripts by RL time
scriptManager.addScript(5f,
new MessageScript("Script should have run 5 seconds after game start."));
scriptManager.addScript(10f,
new MessageScript("Script should have run 10 seconds after game start."));
scriptManager.addScript(15f,
new MessageScript("Script should have run 15 seconds after game start."));
// Test adding scripts that run on a certain date
scriptManager.addScriptAtDate(new GregorianCalendar(205, 11, 2),
new MessageScript("Script should run December 2!"));
// Test Manager lookup code
TimedScriptManager test = TimedScriptManager.getManager(sector);
test.addScript(20f,
new MessageScript("Script should have run 20 seconds after game start."));
}
}
This controller is simple compared to the last one, so I didn't comment it nearly as much. It runs events at the start of each day, week, month, and year.
EventManager.java
package 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();
}
}
}
}