|
Ground of Aces Modding
|
A mission is a flight the player sends aircraft on: it appears on the war map, costs time and fuel, and comes back with one of four endings - the flight went as planned or went wrong, and the crew passed or failed its skill check. Each ending counts as a success or a failure.
| What it is | Best for | |
|---|---|---|
| The Mission Editor | A window inside the game. Fill in fields, press Validate, press Save. | A first mission, and anything whose numbers you want to try out quickly. |
| JSON and Python files | A .json with the numbers and an optional .py with the text and logic. | Everything else - chains, campaigns, missions that react to what the player did. |
They are the same thing underneath. The editor writes exactly the JSON you would have written by hand, so you can start in the editor and keep going in a text editor.
| Page | Contents |
|---|---|
| Mission locations | every place a mission can happen, with coordinates |
| How mods work | what a mod folder is, where it lives, and how to share it |
| Changing game data with a CSV mod | changing the game's numbers rather than adding missions |
| Example missions | shipped mission files to open or save - a single mission, the four-mission Blitz chain, the default script |
The game reads missions from four places, in this order, and later ones win when two carry the same id:
Mods/<your mod>/Missions/, holding .json and .py files. This is how you hand a mission to somebody else. See How mods work.| Platform | Folder |
|---|---|
| Windows | USERPROFILE%\AppData\LocalLow\Blindflug Studios\Ground Of Aces\Missions\ |
| macOS | ~/Library/Application Support/Blindflug Studios/Ground Of Aces/Missions/ |
| Linux | ~/.config/unity3d/Blindflug Studios/Ground Of Aces/Missions/ |
The id of a mission is its file name without the extension - there is no id field inside the file. That last folder always wins, which makes it a good place to try changes out - and a place to remember to clear when a mission of yours seems to ignore the copy in your mod folder.
reload_scripts / reload_mods in development builds.) test_, debug_, _debug or _test is treated as developer content and is not loaded by the shipped game, so do not name a real mission that way.Open the game and choose Mission Editor in the main menu. That opens the mission list: every shipped mission, every mission from an enabled mod, and every mission you saved yourself.
new_mission. Rename it in the Mission Id field before saving.Identity. The mission id, and the tags that decide which maps the mission can appear on. An id must be lower case letters, digits and underscores only.
Tags are free-form labels, not map names. A map lists the tags it supports in its Metadata.json, a mission lists the tags it is written for, and the mission can only turn up on that map when the two lists share at least one tag. Several tags on a mission mean "any of these" - one match is enough.
Type them into the mission tags field as one comma-separated list without spaces: naval,atlantic. A tag follows the same rules as an id - lower case letters, digits and underscores - and the field enforces that as you type, lowercasing capitals and dropping anything else. The match against a map is case-insensitive anyway, so a map that spells its tag Campaign_Debden still counts.
Leaving the list empty is the normal case. An empty list on either side counts as the single tag default, and every shipped map lists default, so an untagged mission is available on all of them. Each shipped map also lists its own name (debden, duxford, lichfield, perranporth), so a mission tagged debden and nothing else appears only there. Tag a mission campaign_debden and it appears only on maps that also list campaign_debden - and nowhere else, not even on the shipped maps, unless you add default alongside it. A map made for a campaign usually lists only its own tag, precisely to keep the regular missions out.
Tags are a mission-only concept. A map chooses the quest line it starts with a different field, StartingQuestIds - see Quests and maps.
Timing and size. How many aircraft the player may send, how long the mission waits on the map before it expires, and how long the flight itself takes. Both times are ranges in game minutes - the game rolls between the two numbers. A time-to-start maximum of 0 means the mission never expires, and a historic mission (below) never expires regardless of what is entered.
Difficulty. The chance the mission goes as planned (0–100), a difficulty rating from 1 to 3, and the enemy strength the intel report shows the player.
Text. The mission name and description, the intel threat report, two mid-flight radio messages (one for a flight that is going to plan, one for a flight that has gone wrong), and one closing message for each of the four endings. Type them straight in - a mission written in the editor does not need a script, because every mission without one runs on the game's shared default mission script.
Locations. Where on the war map the mission can appear. Pick from the list and add as many as you like - the same place cannot be added twice; the game chooses one when the mission is created. The full list, with coordinates, is on Mission locations.
Requirements. Conditions that must all hold before the mission shows up. Four kinds exist:
| Requirement | Value |
|---|---|
RunwayTier | the tier the base's runway must have reached, as a number |
EarliestAppearanceDay | the day number, counted from the start of the campaign, from which the mission may appear |
LatestAppearanceDay | the last day number on which it may appear |
CompletedMission | <mission id>, or <mission id>,success / ,failure / ,cancelled to require a particular result |
Stats. The skills the mission rolls against. You need exactly two: the first is used when the flight goes to plan, the second when it does not.
Outcomes. Exactly four endings, in a fixed order, and what each one gives or costs. Which ending counts as a success is fixed by its position and cannot be edited:
| Slot | Ending | Counts as |
|---|---|---|
| 1 | gone wrong, skill check failed | failure |
| 2 | gone wrong, skill check passed | success |
| 3 | as planned, skill check failed | failure |
| 4 | as planned, skill check passed | success |
The editor shows them top to bottom from the best ending to the worst. Each ending carries any number of effects: CrewMood, CrewConfidence, CrewEnergy (the crew that flew), BaseMood, BaseConfidence, BaseEnergy (everyone at the base), VictoryPoints, and Damage to the aircraft as a min-max range that is rolled per flight.
Historic day. Tick this to make the mission storyline content. A historic mission is kept out of the random weekly draw and instead appears with certainty once its requirements pass: on the exact day you enter, or - if you leave the date empty - together with the next weekly mission, without using up that week's slot. It never expires off the map, and it is gone for good once it has succeeded; a failed or cancelled attempt brings it back, so the next step of a chain stays reachable.
Validate lists everything it can find. Save runs the same check and refuses if anything is an error.
Errors block saving:
| Error | Meaning |
|---|---|
Mission id empty, or has characters other than a-z, 0-9 and _ | |
| Mission id is reserved | a shipped mission already uses it - save under a new id instead |
| Mission id is in use | renaming onto another mission you saved would silently replace it |
| A tag has invalid characters | tags follow the same rules as ids |
| Not exactly four outcomes | the game picks one of four and never checks the count |
| Not exactly two stats | the skill roll reads both |
| Time to start: minimum above maximum | |
| Time to complete: minimum above maximum, or under 5 minutes | |
| A runway tier or day requirement that is not a whole number | |
| A completed-mission requirement the game cannot read |
The reserved-id check only knows the game's own missions. An id that a mod or the Supporter Pack already uses is not reported, and the two silently replace each other by load order - another reason to prefix your ids with your mod's name.
Warnings let you save, and are worth reading anyway:
| Warning | Meaning |
|---|---|
| No locations | the game places the mission at a random location from the shipped list |
| Duplicate location names | |
| A tag no known map carries | the mission is unavailable everywhere - intended only if a script hands it out |
| Difficulty rating outside 1–3 | |
| Fewer than one aircraft slot | nobody can fly it |
| Time to start never expires | the mission sits on the map until flown |
| Historic day ticked but the date is empty, impossible or before 1900 | the mission has no date and behaves like an undated historic mission |
| A stat whose minimum is above its maximum | |
| An outcome effect the game cannot read | that one effect silently never applies |
| Missing narrative text | that slot shows nothing in game |
The narrative-text warning does not appear for shipped missions, which carry translated text rather than text typed into the editor.
.py next to the .json.For either, edit the mission files with a text editor.
A mission is a .json file holding the numbers, and optionally a .py file of the same name holding the text and any logic. The file name is the mission id. Mission scripts live alongside ordinary scripts, in the Mission folder of the scripts directory, or in your mod's Missions/ folder.
Three things catch people out, and all three are limits of the game rather than of your files:
The Mission Editor's Validate button catches the first two, and a clash with a shipped id, before you ever launch. A clash with another mod's id it cannot see.
How much of the script the game uses depends on whether the mission has a .json beside it.
.py next to a .json overrides only the handlers it defines; anything it leaves out falls back to the shared default_mission.py.get_mission_id, in a module named exactly like the mission, is how the game finds the script at all, and get_mission_result decides the ending - without it the mission throws when it finishes. Every other getter has a default when it is missing: the name falls back to the mission id, the description to nothing, time to start to 60 minutes, completion time to 120 minutes, the threat level to Low, the enemy count to 0, the number of progress reports to 3 and the fuel to 100.This is the single most common source of confusion: a handler that works perfectly in a script-only mission is simply ignored once you add a .json.
Every mission script needs from Blindflug.FlightMission import MissionEvent at the top - scripts get no automatic imports. The constant names below are the Python names; where the event string differs it is given in brackets.
| Handler | What it does | When it runs |
|---|---|---|
| get_mission_id | the internal identifier of the mission | always - required for a mission without JSON, inherited from the default script otherwise |
initialize_mission | set the mission up as it is created | always |
| send_mission_event | report progress while the flight is in the air | always |
set_mission_impact | apply consequences once the outcome is known | always |
send_mission_result_event (send_mission_result_text) | react when the mission ends | always, except when cancelled |
send_mission_cancelled_event (send_mission_cancelled_text) | react when the player aborts or the mission expires | always |
| get_mission_result_text | the text in the result window | always |
| get_mission_name | the name in the mission info window | script-only missions |
| get_mission_description_text | the description in the info window | script-only missions |
| get_time_to_start | how long the player has to start it | script-only missions |
| get_completion_time | how long the flight lasts | script-only missions |
| get_threat_level | the threat level in the info window | script-only missions |
| get_enemy_count | how many enemies are present | script-only missions |
| get_mission_events_count | how many progress reports to send | script-only missions |
get_fuel_level | the fuel the mission needs | script-only missions |
| get_mission_result | whether the mission succeeded | script-only missions - required there |
get_mission_icon, calculate_mission_modifiers, calculate_airplane_modifiers or calculate_pilot_modifiers. The constants still exist, but nothing calls them - the modifier system moved into the game itself. Older example scripts implement them at length; that code never runs. get_mission_fatalities no longer exists. Losses are handled through the Damage outcome effect.send_mission_event is not fired on a fixed clock. Each game minute the game computes floor(elapsed / completion time × number of reports) and fires once whenever that number grows, so the reports are spread evenly over the flight and the first call already sees a count of 1. For a mission with JSON the number of reports is fixed at 3, so get_mission_events_count is ignored.A mission without JSON fills in its numbers by calling missions_api from initialize_mission. These are the methods that exist for that, with their exact signatures; every one returns False when the mission or a name is unknown.
| Method | What it sets |
|---|---|
SetOutcome(mission_entity_id, outcome_index, is_success) | which of the four slots (0–3, see the table above) counts as a success |
AddOutcomeEffect(mission_entity_id, outcome_index, effect_type, effect_value) | one effect on a slot; effect_type is one of the effect names above, effect_value a number ("-40") or a range ("70-140") |
AddMissionRequirement(mission_entity_id, requirement_type, value) | one of the four requirements above, by name |
SetTags(mission_entity_id, *tags) | the map tags |
SetMissionStat(mission_entity_id, skill_type, min_value, max_value) | one of the two skill rolls |
SetAsPlannedChance(mission_entity_id, chance) | 0–100; without it the mission always counts as having gone wrong |
SetMissionLocation(mission_entity_id, location_name) | a name from Mission locations - custom coordinates are not possible from a script |
SetMaxAirplaneSlots(mission_entity_id, slots) | how many aircraft may fly |
SetDifficulty(mission_entity_id, difficulty_rating) | the 1–3 star rating |
CreateMission(flight_mission_id) | puts another mission on the map now, bypassing the weekly draw - how a script hands out a follow-up that JSON requirements cannot express |
For reading the state of a running mission see GetMissionId, GetNumberOfEventsSent, MakeAsPlannedRoll, GetAsPlannedRoll, MakeSkillRoll, GetSkillRoll, GetMissionAirplaneFormation, GetEnemyCount and IsMissionSuccessful on missions_api.
The files below are the game's own missions, exactly as they ship. Open one to see what a finished mission looks like, or save it into Mods/<your mod>/Missions/ under a new file name and change it from there.
missions/<id>/mission_name, mission_description, intel_threat_report, ap_transmission, gw_transmission, ap_success_transmission, ap_failure_transmission, gw_success_transmission and gw_failure_transmission. A copy under a new id therefore shows no text until you give it some. Either fill the _raw* fields at the bottom of the file, or put a .py next to it that registers terms with localization.SetTerm(language, key, text). Keeping the shipped file name instead replaces the shipped mission, text and all.| File | What it is |
|---|---|
| calm_supply_drop.json | A transport mission with no script of its own: it appears from day 20 on a tier 3 runway, can happen at one of four places, and runs on the shared default script below. Everything a mission needs is in this one file. |
The quest example_patrol_chain on the quests page (see Example quests) waits for this mission to succeed, so the two can be tested together.
The four Blitz missions form the storyline the game plays from November 1940. Each one is historic and dated, so it appears on its exact day once the one before it has succeeded, and each is gated on that success with requirement type 3 (CompletedMission):
| File | Role |
|---|---|
| german_blitz_1_long_nights.json | the opening mission, tied to a date |
| german_blitz_2_aspirin.json | follows a success of the first |
| german_blitz_3_morning_after.json | follows a success of the second |
| german_blitz_4_answering_in_kind.json | follows a success of the third |
Compare the _missionRequirements blocks at the top of the four files: they differ only in the runway tier and the mission each one waits for. Their _tags list names every shipped map and default, so the chain runs everywhere; drop default and keep a single map's tag to restrict a chain of your own to that map.
| File | What it is |
|---|---|
| default_mission.py | The script every mission without a .py of its own runs on. It answers get_mission_id, rolls once whether the flight goes as planned at the first progress report, rolls the skill check at the second, and picks the transmission and result text keys from the outcome. Copy it as the starting point for a mission that needs its own logic. |
See Scripting for how event handlers work, and Mission event handlers above for what each handler in the file is for.