You can still have most of the work done in another thread; only the actual alterations to the cargo need to be done in the main thread.
Basically, your second thread would grab the stacks copy, do the needed analysis, etc, but instead of making alterations to the station's contents, you'd want to put a list of instructions into a queue that's handled by an EveryFrameScript in the main thread (this queue variable needs to be thread-safe, check out
ConcurrentLinkedQueue).
Here's a simple example of an EveryFrameScript that would check a thread-safe queue for instructions every frame (not tested!):
ExampleQueue.java:package data.scripts.plugins;
import com.fs.starfarer.api.EveryFrameScript;
import data.scripts.commands.BaseCommand;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
// Keep a reference to this object in your mod's
// master script, otherwise it's worthless
public class ExampleQueue implements EveryFrameScript
{
private final Queue queuedCommands = new ConcurrentLinkedQueue();
@Override
public boolean isDone()
{
return false;
}
@Override
public boolean runWhilePaused()
{
return false;
}
@Override
public void advance(float amount)
{
synchronized (queuedCommands)
{
BaseCommand tmp;
while (!queuedCommands.isEmpty())
{
tmp = (BaseCommand) queuedCommands.remove();
tmp.executeCommand();
}
}
}
public synchronized void addCommandToQueue(BaseCommand command)
{
queuedCommands.add(command);
}
public synchronized boolean isQueueEmpty()
{
return queuedCommands.isEmpty();
}
}
The queue holds a list of commands that need to be executed in the main thread. The commands are defined by a simple interface:
BaseCommand.java:package data.scripts.commands;
public interface BaseCommand
{
public void executeCommand();
}
And an example BaseCommand implementation (this one for removing cargo, since that's what brought this up):
RemoveCargo.java:package data.scripts.commands;
import com.fs.starfarer.api.campaign.CargoAPI;
public class RemoveCargo implements BaseCommand
{
private CargoAPI cargo;
private Object data;
private CargoAPI.CargoItemType type;
private float toRemove;
public RemoveCargo(CargoAPI cargo, Object stackData,
CargoAPI.CargoItemType stackType, float toRemove)
{
this.cargo = cargo;
this.data = stackData;
this.type = stackType;
this.toRemove = toRemove;
}
@Override
public void executeCommand()
{
cargo.removeItems(type, data, toRemove);
}
}
So if your second thread needs to remove 500 green crew from a station's cargo, instead of doing so directly it'd use something like the following:
ClassThatKeepsTrackOfVariables.getCommandQueue().addCommandToQueue(new RemoveCargo(station.getCargo(), "green_crew", CargoItemType.RESOURCES, 500f));
And the crew would be removed the next game frame.
An important thing to note, though, is that you'd want to check isQueueEmpty() before starting your analysis 'frame'. Otherwise you'll have problems where your second thread runs multiple times between game frames and ends up removing 1000+ crew because the RemoveCargo command hasn't run yet, so the station still registers as having too much crew.
This method is how the console mod handles being multi-threaded, at least. There are probably better ways of doing it, and I am by no means an expert at threading.