Added documentation about saved games and transients, cleaned up existing unique documentation

This commit is contained in:
Yair Morgenstern
2024-04-02 19:17:43 +03:00
parent c2a2bcb417
commit be17bbb389
3 changed files with 35 additions and 73 deletions

View File

@ -0,0 +1,27 @@
# Saved games and transients
Unciv is a game where many things are interconnected. Each map unit, for example, belongs to a *civ*, is located on a *tile*, can have several *promotions* and "inherits" from a *base unit*.
When saving a game state, we want it to be as small as possible - so we limit the information saved to the bare minimum. We save names instead of pointers, and anything that can be recalculated is simply not saved.
But during runtime, we *need* these links for performance - why perform a lookup every time if we can save a reference on the object?
Classes are therefore *freeze dried* on serialization, and *rehydrated* for runtime. Since these fields are marked in Kotlin as @Transient fields, we call the rehydration function `setTransients`.
Take the map unit for example. How can we calculate the uniques for that unit? They can come from several places:
- Base unit uniques
- Promotions
- Civ-wide uniques
So from the save file, we get the civ name, unit name, and promotion names; for runtime, we'll want a reference to the civ, base unit, and promotions.
We can find these by looking up the civ in the game, and the unit and promotions from the ruleset.
The civ itself - a game object - references the nation - a ruleset object, which is another link. The base unit references the unit type, another link.
The nation, base unit, and promotions, all contain *uniques* - which are saved as strings. For performance, these too get saved at runtime as Unique instances - which contain the unique type, the parameters, conditionals, etc.
Beyond the fact that each ruleset object retains a "hydrated" list of its uniques, the unit's uniques don't change very often - so we can add *yet another* layer of caching by saving all the unit's uniques, and rebuilding this every time there's a change
All of this is VITAL for performance - Unciv is built to run on potatoes, and even hash lookups are expensive when performed often, not to mention Regexes required for Unique parsing!

View File

@ -26,7 +26,6 @@ A common suggestion that we get (by people with little familiarity with the proj
Here are some ways that we managed to go wrong in the past:
- Putting all languages into the same file ("one big translation dictionary") - when multiple people edit this file for different languages, they can conflict with each other. Separate to different files for simpler management.
- Using json - json is great for machines, but less so for humans, who can easily make mistakes. Json format is surprisingly finnicky, miss a closing " and the whole file is now unreadable.
The format we decided to go for is one file per language, delimited by " = " for visual separation, in a .properties file. Lines starting in # are considered comments, so we can add comments for translators.
@ -62,72 +61,3 @@ To translate a text like "[Temple] has been built in [Rome]", therefore, we need
The translation generation reads information from "a ruleset", i.e. the set of jsons defining the game's objects.
Every mod is also a ruleset, either replacing or adding to the base ruleset defined in the game.
This means that the same translation generation that we do for the base game can also be applied to mods, and so each modder can decide (from within the game) to generate translation files for his mod, and since mods are uploaded to Github to be widely available as part of the mod release methodology, translators will be able to translate those files the exact same way that they translate Unciv's base ruleset.
## Uniques
### Moddable unique effects
Every object in Unciv can include "uniques" - a list of strings, each granting a unique effect that is not applicable for every object of its type.
For example, the Palace building has the unique "Indicates the capital city", and the settler has the unique "Founds a new city".
This allows us to share effects between multiple units, and to avoid hardcoding and allow modders to add *any* effect to *any* object.
Here too we encounter the problem of "generic" uniques - how can we have these effects grant a building, some stats, etc, using the same unique for all objects? Why, with placeholders of course! For example, one building has "Requires a [Library] in all cities", where "Library" can be replaced with any other building for similar effects. We can then extract the parameters from the unique at runtime, to know how to resolve the unique's effects.
Since the translation template is the same as the unique template, these uniques are instantly translatable as well!
We do have a slight problem, though - since translation texts come directly from the json files, and the json files have "Requires a [Library] in all cities", how do we tell the translators not to directly translate "Library" but the take the parameter name verbatim?
Well, 95% of translation parameters fit nicely into a certain type - units, buildings, techs, terrains etc. So we can search for an object with than name, and since we find a Library building, we can put "Requires a [buildingName] in all cities = " as our translation line.
### Filters
As time went on, we noticed that many of our "uniques" weren't so unique after all. Many were the same but with slightly different conditions. One affects all cities, one only coastal cities, and one only the city the building is built in. One affects Mounted units, one affects wounded units, one affects all water units, etc. We started compiling these conditions into "filters", which limited the number of uniques while expanding their range considerably.
Take the following example unique for a building: "[+1 Food] from [Deer] tiles [in this city]".
In its "placeholder" form, this is "[stats] from [tileFilter] tiles [cityFilter]".
stats can accept any list of stats, e.g. '-2 Gold, +1 Science', '+3 Culture', etc.
tileFilter can accept any number of tile parameters (base terrain e.g. 'Plains', terrain type eg. 'Land'/'Water', terrain features e.g. 'Forest', improvements e.g. 'Mine', resources e.g. 'Iron'.
cityFilter can accept 'in this city', 'in all cities', 'in capital', 'in coastal cities', etc.
There are also filters for units, all acceptable values are documented [here](../Modders/unique%20parameters).
### Unique management with Enums
The further along we go, the more generic the uniques become, and the more of them there are.
Older uniques become new ones, by being merged or made more generic, and the older ones are deprecated. Deprecation notices are put on Discord, but a one-time message is easy to miss, and if you come back after a while you don't know what's changed.
Modders discover during gameplay that the values they put for uniques were incorrect.
All these problems are solved with a single solution - since all uniques are defined by their text, we can create an enum with ALL existing uniques, which lets us:
- Find all usages of a unique in the IDE instantly
- Mark deprecated uniques as such using `@Deprecated("as of <versionNumber">)` for devs (and modders!)
- Compare uniques using enum values, which is faster
What's more, with a little bit of autodetection magic, we can determine the *type* of the parameter using its text.
Using the above example, "[stats] from [tileFilter] tiles [cityFilter]", we can tell by the names of the parameters what each one is supposed to be,.
We can then check at loading time for each unique, if its parameter values matches the parameter type it's supposed to have, which lets us catch incorrect parameters.
The "autodetection" of parameter types for translations can also be fed from here, leading to much more accurate translation texts - instead of detecting from an example (e.g. "Requires a [Library] in all cities" from the json), we now use a dev-inputted value like "Requires a [buildingName] in all cities". This allows us to accept multiple types, like for e.g. "Requires [buildingName/techName/policyName]".
Deprecated values can be detected due to the `@Deprecated` annotation, and can be displayed to the modders when loading the mod, together with the correct replacement.
### Conditionals
Beyond the existing filters for units, buildings, tiles etc, there are some conditions that are global. For example, uniques that take effect when the empire is happy; when a tech has been researched; when the empire is at war; etc.
Rather than being 'build in' to specific uniques, these conditions can be seen as extensions of existing uniques and thus globally relevant.
For example, instead of "[+1 Production] [in all cities] when empire is happy", we can extract the conditional to "[+1 Production] [in all cities] <when empire is happy>". This does two things:
A. Turns the 'extra' unique back into a regular "[stats] [cityFilter]" unique
B. Turns the conditional into an extra piece that can be added onto any other unique
Conditionals have a lot of nuance, especially regarding translation and ordering, so work in that field is more gradual.
### What's next?
We have yet to fully map all existing uniques and convert all textual references in the code to Enum usages, and have yet to extract all conditionals from their uniques.
We already have a map of what uniques can be put on what objects - it won't take much to add that check as well and warn against uniques that are put on the wrong sorts of objects.
Once we build the full inventory of the uniques, instead of the wiki page that needs to be updated manually we'll be able to generate a list of all acceptable uniques and their parameters directly from the source of truth. Put that in a webpage, add hover-links for each parameter type, generate and upload to github.io every version, and watch the magic happen.
We'll also be able to notify modders if they use "unknown" uniques.

View File

@ -14,9 +14,11 @@ Game objects should have *concrete* uniques (parameters filled in)
Every parameter in square brackets, is defined by its type, a list of which is available [here](../Modders/Unique-parameters.md) - each parameter type has its own text value, e.g. "amount" means an integer.
That determines possible values that this parameter can contain, e.g. "amount" should only contain strings that can be serialized as integers.
Concrete uniques that contain *incorrect values* (e.g. `"Gain [three] [money]"`) are warned against in the mod checker, and when starting a new game with the mod
Concrete uniques that contain *incorrect values* (e.g. `"Gain [three] [money]"`) are warned against in the mod checker, and if they're serious enough, also when starting a new game with the mod
### About Conditionals and Modifiers
Sometimes uniques are deprecated - Unciv provides autoupdating, meaning you only need to click a button to update the deprecated uniques in your mod!
### Conditionals and Modifiers
Uniques can be modified to do certain things, using special Uniques that are shown in the [uniques list](../Modders/uniques.md) within `<these brackets>`.
@ -24,7 +26,10 @@ This is done by adding these modifiers after the unique like so: `"Gain [30] [Go
The most common type of modifier is a conditional - basically limiting the unique to only apply under certain conditions - so all modifiers are sometimes refered to as conditionals.
Other more specialized types of conditionals exist, and each one has a dedicated explaination in the uniques list linked above.
Other more specialized types of modifiers exist:
- Trigger uniques can get *under what circumstances they activate*
- Unit actions can get *costs, side effects, and limited uses*
As you can see, these conditionals *also* can contain parameters, and these follow the same rules for parameters as the regular uniques.