First Look at Modding
We’re getting close to releasing the alpha, and I’m happy to say that you’ll be able to start modding Starfarer in the very first release.
The entire focus of the alpha is on combat, exposed through a set of missions. In each mission, two fleets face each other in a scenario you’ll run into naturally in the full game. For example, the player might command a freighter fleet during a blockade run, or a small task force ambushing a carrier group inside a nebula.
Missions are the only aspect that will be moddable in the first release, but it’s just a start – we’ll expand modding capabilities to include ships, AI, factions, etc. Technically, I suppose you could add new ships in the first release (and easily tweak existing ones), since they’re all data-driven… but it’s much nicer with an actual editor. (Yes, we’ll release some modding tools.)
Ahem… back to missions!
Each mission is defined with a couple of files you can tweak using a text editor, or copy to use as a baseline for a new mission. To see what a mission is made up of, let’s take a look at a screenshot of the mission selection screen, with the “Turning the Tables” mission selected.
Mission selection screen – click to enlarge
Alright, now, things are about to get technical. And somewhat lengthy. Consider yourself warned.
First, let’s take a look at the directory structure:
data/missions mission_list.json m1/ MissionDefinition.java descriptor.json icon.jpg mission_text.txt m2/ ... m3/ ... |
The master list of missions you see on the left panel in the screenshot is defined in mission_list.json. The bits that show up in the list – the title, difficulty, and icon – are defined in descriptor.json (see json.org for details on the JSON format. Most of Starfarer’s data files use it.)
Here’s what the descriptor for “Turning the Tables” looks like:
{ "title":"Turning the Tables", "difficulty":"EASY", "icon":"icon.jpg", } |
The mission description lives in a separate text file – mission_text.txt – because multi-line strings are a pain to edit within a JSON file.
Now, the fun part – the actual contents of the mission. These are defined using a script written in Java – MissionDefinition.java. Because of that, mission contents can be dynamic – for example, you can set up a mission to be a random encounter with pirates and have the fleets be re-rolled every time the player clicks on it, or randomize some terrain features.
All of the work is done in a single function, defineMission, which is passed in a MissionDefinitionAPI object that it uses to add ships, terrain features, objectives, etc. Links are to the starfarer.api javadoc. A lot of the classes/interfaces you see there are stubs that will be fleshed out as development progresses.
The API binary and source will be available for download soon. It’s not strictly needed for modding – you could edit the below file in Notepad and it would work – but having it will make it easy to work in a Java IDE of your choice.
Here’s the script that creates the “Turning the Tables” mission. It’s heavily commented to let you know what’s going on, and there’s a screenshot of the map it creates after the script.
package data.missions.m1; import com.fs.starfarer.api.combat.BattleObjectiveAPI; import com.fs.starfarer.api.fleet.FleetGoal; import com.fs.starfarer.api.fleet.FleetMemberType; import com.fs.starfarer.api.mission.FleetSide; import com.fs.starfarer.api.mission.MissionDefinitionAPI; import com.fs.starfarer.api.mission.MissionDefinitionPlugin; public class MissionDefinition implements MissionDefinitionPlugin { public void defineMission(MissionDefinitionAPI api) { // Set up the fleets so we can add ships and fighter wings to them. // In this scenario, the fleets are attacking each other, but // in other scenarios, a fleet may be defending or trying to escape api.initFleet(FleetSide.PLAYER, "ISS", FleetGoal.ATTACK, false); api.initFleet(FleetSide.ENEMY, "", FleetGoal.ATTACK, true); // Set a small blurb for each fleet that shows up on the mission detail and // mission results screens to identify each side api.setFleetTagline(FleetSide.PLAYER, "ISS Hamatsu and ISS Black Star with drone escort"); api.setFleetTagline(FleetSide.ENEMY, "Suspected Cult of Lud forces"); // These show up as items in the bulleted list under // "Tactical Objectives" on the mission detail screen api.addBriefingItem("Defeat all enemy forces"); api.addBriefingItem("ISS Black Star & ISS Hamatsu must survive"); api.addBriefingItem("Fighter drones are expendable"); // Set up the player's fleet. Variant names come from the // files in data/variants and data/variants/fighters api.addToFleet(FleetSide.PLAYER, "hammerhead_Balanced", FleetMemberType.SHIP, "ISS Black Star", true); api.addToFleet(FleetSide.PLAYER, "venture_Balanced", FleetMemberType.SHIP, "ISS Hamatsu", false); api.addToFleet(FleetSide.PLAYER, "wasp_wing", FleetMemberType.FIGHTER_WING, false); // Mark both ships as essential - losing either one results // in mission failure. Could also be set on an enemy ship, // in which case destroying it would result in a win. api.defeatOnShipLoss("ISS Black Star"); api.defeatOnShipLoss("ISS Hamatsu"); // Set up the enemy fleet. // It's got more ships than the player's, but they're not as strong. api.addToFleet(FleetSide.ENEMY, "condor_Strike", FleetMemberType.SHIP, false); api.addToFleet(FleetSide.ENEMY, "lasher_Support", FleetMemberType.SHIP, false); api.addToFleet(FleetSide.ENEMY, "lasher_Support", FleetMemberType.SHIP, false); api.addToFleet(FleetSide.ENEMY, "hound_Assault", FleetMemberType.SHIP, false); api.addToFleet(FleetSide.ENEMY, "hound_Assault", FleetMemberType.SHIP, false); api.addToFleet(FleetSide.ENEMY, "piranha_wing", FleetMemberType.FIGHTER_WING, false); api.addToFleet(FleetSide.ENEMY, "broadsword_wing", FleetMemberType.FIGHTER_WING, false); api.addToFleet(FleetSide.ENEMY, "talon_wing", FleetMemberType.FIGHTER_WING, false); api.addToFleet(FleetSide.ENEMY, "talon_wing", FleetMemberType.FIGHTER_WING, false); api.addToFleet(FleetSide.ENEMY, "talon_wing", FleetMemberType.FIGHTER_WING, false); // Set up the map. // 12000x8000 is actually somewhat small, making for a faster-paced mission. float width = 12000f; float height = 8000f; api.initMap((float)-width/2f, (float)width/2f, (float)-height/2f, (float)height/2f); float minX = -width/2; float minY = -height/2; // All the addXXX methods take a pair of coordinates followed by data for // whatever object is being added. // Add two big nebula clouds api.addNebula(minX + width * 0.75f, minY + height * 0.5f, 2000); api.addNebula(minX + width * 0.25f, minY + height * 0.5f, 1000); // And a few random ones to spice up the playing field. // A similar approach can be used to randomize everything // else, including fleet composition. for (int i = 0; i < 5; i++) { float x = (float) Math.random() * width - width/2; float y = (float) Math.random() * height - height/2; float radius = 100f + (float) Math.random() * 400f; api.addNebula(x, y, radius); } // Add objectives. These can be captured by each side // and provide stat bonuses and extra command points to // bring in reinforcements. // Reinforcements only matter for large fleets - in this // case, assuming a 100 command point battle size, // both fleets will be able to deploy fully right away. api.addObjective(minX + width * 0.5f, minY + height * 0.5f, "sensor_array", BattleObjectiveAPI.Importance.CRITICAL); api.addObjective(minX + width * 0.75f, minY + height * 0.75f, "comm_relay", BattleObjectiveAPI.Importance.IMPORTANT); api.addObjective(minX + width * 0.25f, minY + height * 0.5f, "nav_buoy", BattleObjectiveAPI.Importance.MINOR); api.addObjective(minX + width * 0.75f, minY + height * 0.25f, "comm_relay", BattleObjectiveAPI.Importance.IMPORTANT); // Add an asteroid field going diagonally across the // battlefield, 2000 pixels wide, with a maximum of // 100 asteroids in it. // 20-70 is the range of asteroid speeds. api.addAsteroidField(minY, minY, 45, 2000f, 20f, 70f, 100); // Add some planets. These are defined in data/config/planets.json. api.addPlanet(minX + width * 0.2f, minY + height * 0.8f, 320f, "star_yellow", 300f); api.addPlanet(minX + width * 0.8f, minY + height * 0.8f, 256f, "desert", 250f); api.addPlanet(minX + width * 0.55f, minY + height * 0.25f, 200f, "cryovolcanic", 200f); } } |
Screenshot of map, viewed in the warroom with UI elements hidden. Click to enlarge.
I think next post will be a video of a full playthrough of this mission to show a slice of continuous gameplay.
Questions? Comments? If you’ve got an interest in modding Starfarer, I’d love to hear from you.
Tags: alpha, fleet, java, json, mission, modding, objective, planet, plugin, scenario, script
Wow, surprised to see an API coming with the alpha. Glad to see you went the compiled java approach instead of a scripting one. Can’t wait to see the video!
It’s java … scripting 🙂
Awesome. Did I remember hearing that the mission scripts were originally going to be Groovy-based, instead of full-out Java?
Yeah, you’re right. Didn’t think anyone would pick up on that!
I did try out Groovy for a while, but had lots of minor issues with it. Main reason was the much nicer IDE support for Java, though. The Groovy Eclipse plugin isn’t quite there.
Ended up using the Janino embedded compiler, works great so far.
I’ve seen some people build a “Groovy script runner” system that includes the Groovy runtime jars in their distribution in order to avoid requiring the Groovy runtime to be installed for clients. I’d say it’s worth revisiting support for Groovy, as I think it ends up being a bit easier for clients to script in. It also allows gives you a little more control and flexibility in terms of what aspects of your overall codebase to expose to client scripts.
That’s exactly what I had set up, complete with the 5mb runtime.
I had the same hopes – more control, flexibility, easier for clients – but when I tried using it, those weren’t there. Instead I ran into a quagmire of little integration and IDE setup issues (some of which aren’t Groovy’s fault).
I think ease of use has more to do with the API’s you expose – the MissionDefinition script would look much the same in Java or Groovy.
Are there eventually going to be more complex behaviors other than ATTACK, DEFEND, and FLEE? I mean, that could be an irrelevant question, but I figured I’d throw it out there.
Wow, this looks epic, can’t wait for the alpha! Java’s probably my best language as well, so really excited about the modding.
Mike: Good question – I really haven’t explained what those are anywhere.
ATTACK/DEFEND/etc aren’t specific ship behaviors, but overall fleet goals. The combination of goals determines some specific rules that apply in the battle and also affect the outcome.
Say two fleets meet, and one is outmatched. It’s going to try to escape, while the other fleet tries to stop it from doing that – the goals are decided by each side before battle starts. In battle, the escaping fleet might capture a few objectives (such as a nav buoy to get a speed boost) and then try to punch through enemy lines to retreat. Any ships that can’t do that will be captured by the other fleet after the battle is over, while the rest get away.
And yes, there’ll be some other goals – to represent scenarios where a fleet attacks fixed defensive installations, for example.
Tamunshin: Cool!
So, are things limited to two fleets per mission (1 player, 1 enemy)?
Can a fleet have multiple goals (guessing not, since it’s an enum)?
Yep, two sides to a battle. It’s only hardcoded to two sides in a few places, though 🙂 Some mechanism for allies is likely but wasn’t needed yet.
And yeah, one goal per fleet. With the current goal set, about the only combination that makes sense is defend + escape, and even that is really just “escape”, in terms of how it would play out.
That makes a lot of sense. Things seem really well thought out in terms of goals, extensibility, and design/architecture. Kudos!
[…] can also do some modding, if you’re so inclined (custom […]
[…] can also do some modding, if you’re so inclined (custom […]