Reorganized wiki

This commit is contained in:
Yair Morgenstern
2022-02-23 21:06:32 +02:00
parent 4f3fa7b92c
commit e99ba8cee7
28 changed files with 123 additions and 162 deletions

View File

@ -0,0 +1,30 @@
If you also have JDK 11 installed, you can compile Unciv on your own by cloning (or downloading and unzipping) the project, opening a terminal in the Unciv folder and run the following commands:
### Windows
Running: `gradlew desktop:run`
Building: `gradlew desktop:dist`
### Linux/Mac OS
Running: `./gradlew desktop:run`
Building: `./gradlew desktop:dist`
If the terminal returns `Permission denied` or `Command not found` on Mac/Linux, run `chmod +x ./gradlew` first. *This is a one-time procedure.*
If you get an error that Android SDK folder wasn't found, firstly install it by doing in terminal:
`sudo apt update && sudo apt install android-sdk` (Debian, Ubuntu, Mint etc.)
After that you should put its folder to the file `local.properties` by adding this line:
`sdk.dir = /path/to/android/sdk` which can be `/usr/lib/android-sdk` or something other.
If during the first launch it throws an error that the JDK version is wrong try [this JDK installation](https://www.azul.com/downloads/zulu-community/?package=jdk).
Gradle may take up to several minutes to download files. Be patient.
After building, the output .JAR file should be in /desktop/build/libs/Unciv.jar
For actual development, you'll probably need to download Android Studio and build it yourself - see Contributing :)

View File

@ -0,0 +1,15 @@
As an open-source project, there will be a lot of eyes on our code.
The main purpose of having a coding standard is for the code to be as immediately readable as possible to as many potential contributors, and hence most of it focuses on defaulting to coding structures that exist in other similar languages (Java, C#) when possible.
## Don't use `.let{}` and `?:`
Kotlin is made greater for being strict with nullability. Don't let this fact confuse people new to it. These can be simply replaced by `if(x!=null)` which is much more readable. They all probably compile to the same bytecode anyway, so when in doubt - readability.
## `for(item in list)` and not `list.forEach{}`
For loops go waaaay back, forEach doesn't. As an added bonus, I'm pretty sure that because forEach accepts a function parameter, then when debugging it won't automatically step into these lines, unlike for.
## Avoid premature abstraction
There's no need to create an interface if there is only one implementation of that interface. Doing so obfuscates the actual code that's running and increases the Time To Relevant Code. If abstraction becomes necessary later, we can always do it later.

View File

@ -0,0 +1,71 @@
# From code to deployment
So, your code works! You've solved all the bugs and now you just need to get it out to everyone!
So, how does THAT work?
The process has two major parts, one is "Getting your code in the main repository" and the other is "Deploying versions" - as a developer, you'll be taking an active part in the first process, but the second process is on me =)
## Getting your code in the main repo
* First off, push your changes with Git to your own branch at https://github.com/YourUsername/Unciv.git. I hope you've been doing this during development too, but that's none of my business \*sips tea\*
* Issue a pull request from https://github.com/YourUsername/Unciv - from the Pull Requests is the simplest
* The Travis build will check that your proposed change builds properly and passes all tests
* I'll go over your pull request and will ask questions and request changes - this is not only for code quality and standard, it's mostly so you can learn how the repo works for the next change you make =)
* When everything looks good, I'll merge your code in and it'll enter the next release!
## Deploying versions
When I'm ready to release a new version I:
* Comment "merge translations" in one of the open PRs tagged as 'mergeable translation' to trigger the translation branch creation, add a "summary" comment to trigger summary generation, merge the PR and delete the branch (so next version translation branch starts fresh)
* From my workstation - pull the latest changes and run the [translation generation](../Other/Translating.md#translation-generation---for-developers)
* Change the versionCode and versionName in the Android build.gradle so that Google Play and F-droid can recognize that it's a different release
* Add an entry in the changelog.md done, WITHOUT hashtags, and less than 500 characters (that's the limit for Google play entries). The formatting needs to be exact or the text sent to Discord, the Github release etc. won't be complete.
* Add a tag to the commit of the version. When the [Github action](https://github.com/yairm210/Unciv/actions/workflows/buildAndDeploy.yml) sees that we've added a tag, it will run a build, and this time (because of the configuration we put in the [yml file](/.github/workflows/buildAndDeploy.yml) file), it will:
* Pack a .jar file, which will work for every operating system with Java
* Use Linux and Windows JDKs to create standalone zips for 32 and 64 bit systems, because we can't rely on the fact that users will have a JRE
* Download [Butler](https://itch.io/docs/butler/installing.html) and use it to [push](https://itch.io/docs/butler/pushing.html) the new versions to the [itch.io page](https://yairm210.itch.io/unciv)
* Read the changelog.md file to get the changes for the latest version
* Upload all of these files to a new release on Github, with the release notes, which will get added to the [Releases](https://github.com/yairm210/Unciv/releases) page
* Send an announcement on the Discord server of the version release and release notes via webhook
* Pack, Sign, and Upload a new APK to the Google Play Console at 10% rollout
* The F-Droid bot checks periodically if we added a new tag. When it recognizes that we did, it will update the [yaml file here](https://gitlab.com/fdroid/fdroiddata/blob/master/metadata/com.unciv.app.yml)
* When the bot next runs and sees that there's a version it doesn't have a release for, it will attempt to build the new release. The log of the build will be added [here](https://f-droid.org/wiki/page/com.unciv.app/lastbuild) (redirects to the latest build), and the new release will eventually be available [here](https://f-droid.org/en/packages/com.unciv.app/)
## About Google Play publishing
+We start at a 10% rollout, after a day with no major problems go to 30%, and after another day to 100%. If you were counting that means that most players will get the new version after 2+ days.
+
+If there were problems, we halt the current rollout, fix the problems, and release a patch version, which starts at 10% again.
+
+Dear future me - the automation was extremely annoying guesswork to set up, so the facts you need to know are:
- There is a user at the [Google Cloud Platform Account Manager](https://console.cloud.google.com/iam-admin/iam) called Unciv_Upload_Account. There is an access key to this account, in json, stored as the Github secret GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.
- This user was granted ADMIN permissions to the Google Play (after much trial and error since nothing else seemed to work) under User > Users and Permissions. Under Manage > Account permissions, you can see that it has Admin.
## Updating the wiki
Pages for the [Unciv Github Wiki](https://github.com/yairm210/Unciv/wiki/) are kept in the main repository under [/docs/wiki](/docs/wiki).
The process to edit the wiki is as follows:
1. Open a pull request in the main Unciv repository that changes files under [/docs/wiki](/docs/wiki).
2. Once the pull request is merged, an account with commit privileges on the Unciv repository leaves a comment saying "`update wiki`".
3. This comment triggers a bot to copy all the wiki files from the main repository into the Github wiki, with a link back to the PR in its commit message for credit.
Doing things this way has several distinct advantages over using the Github Wiki web interface directly:
* Changes can be proposed via PR and proofread or fact-checked.
* A proper MarkDown editor or IDE can be used to write the wiki, bringing faster editing, clickable links while editing, better live HTML preview, and automatic detection of problems like broken links.
* The wiki files can also be browsed at https://github.com/yairm210/Unciv/tree/master/docs/wiki.
* Auto-generated documentation made by the build process can be placed directly in the wiki.
However, it also imposes a couple of conventions about how links should best be formatted:
|Link type|Format|Example|
|---|---|---|
|Inter-wiki|Should begin with "./", and include ".md".|[`./Mods.md#other`](../Modders/Mods.md#other)|
|Code or asset file|Should begin with "/", and be relative to the project root.|[`/android/assets/game.png`](/game.png)|
These formats will allow IDEs like Android studio to resolve these links and check for broken links, while also working on the [Github code browser](https://github.com/yairm210/Unciv/tree/master/docs/wiki).
The bot that updates the wiki from the main repository automatically translates them into formats that are compatible with Github Wikis, which have somewhat non-standard requirements.

View File

@ -0,0 +1,176 @@
# Tips and tricks for making a LibGDX game
Here are a bunch of things I've learned from by brief excursion into the world of game making.
Some of our will be obvious to you, some will not.
## Use Kotlin
Unciv started its life as a Unity project in C#, was shifted to Java and LibGDX, and finally to Kotlin.
I regret every minute that I spent writing events in Java, this is probably the most significant change that your application could see.
## Use Scene2d
Unless you plan on creating images on the fly, you'll probably be using prerendered assets.
Placing them manually is akin to manually positioning html tags, instead of using html heirarchy and css to guide positions.
So too is Scene2d - as a placement framework. it's relatively simple to understand, especially when you...
## Ignore Horizontal and Vertical groups - use Table
I personally found that table has all the functionality of the above, and more.
Each class has a different syntax too, so I found it much simpler to just stick with Table for everything.
Table does just about EVERYTHING! It's insanely amazing!
## If your game is getting slow, use the Android profiler in Android Studio
The top-down CPU chart is the best code profiler I've ever seen, use it to your advantage!
### Cache everything
Caching is a trade-off between purer, state-agnostic code and higher performance.
Coming from a PC background, I automatically assume that anything less than O(n^2) is less than a milisecond and therefore, not a cachinhg candidate.
This is not so in mobile development.
This becomes especially relevant when you need to save and load game data which has lots of connected parts - you have to avoid circular references, and you want to minimise the save size, but you need to reconstruct the missing links when loading.
### Minimize String operations
All the tip and tricks you've heard to minimize String operations? Use them!
String constants should be consts, use StringBuilders (or just ArrayLists of strings that you later .joinToString())
### Sequences everywhere!
One thing I did not expect to be such an issue is intermediate lists when sorting and mapping.
But apparently, the memory allocation for these tasks is Serious Business.
So whenever possible, take your list and .asSequence() it before actiating list operations - this results in huge savings of both time and memory!
The only time you shouldn't be doing this, though, is when you want to cache the specific values for future use -
sequences will go through the whole process every time you iterate on them, so just .toList() them when you've gotten the final results!
# General tips for making an Open Source game
## Lower the entry bar - for both programmers and players
I think that most Open Source games suffer from this problem - those that are in are way in, but those that are out and want to join have to learn the ecosystem.
Documentation is a big issue here, but so are detailed instructions - and I mean "Spoonfeeding".
Treat new developers as if they've never used Git before - it's possible they haven't!
Explain how to dowload the sourecode, the tools, how to get the game running locally, how to make changes and how to submit them.
Same think with new players - getting the game up and running should be AS SIMPLE AS HUMANLY POSSIBLE - you want people to play your game, don't you?
This includes:
- Source-To-Executable automation - I use Travis
- Play stores and the like
- Internal game tutorials - your players will NEVER BE SATISFIED with this last point, but at least do what you candidate
## Community, Community, Community!
I, personally, underestimated this point for about a year after launch.
I communicated with players through the Google Play Store and Github issues, and that seemed to be enough.
It was only after repeated urgings from players that I opened a Discord server - and that gradually lead to a massive change!
You see, it's not ABOUT programmer-to-player interaction. There will always be a small number of core devs relative to the large playerbase.
The key to the community is the player-to-player interaction. Explaining things, questions, ideas, things that players bounc off each other,
not only make the amorphous community a better pllace, but actually lead to a better game!
Another think to remember is that there's a larger community around you - the Open Source community, the Linux community, etc.
There are lots of people who will play your game only because it's open source, and it also means they don't have as many options.
For example...
- Being the best 4X game means competing with the biggest names out there
- Being the best 4X game for Linux means many less competitors, but All The Cool Kids (tm) are multiplatforming nowadays so you're still outperformed.
- Being the best Open Source 4X game means about 5 competitors, and no money is involved either so the average entry is not as polished.
- Being the best Open Source 4X game for Android... means having so few competitors that it's totally doable.
## Everything is marketing.
Your game's name, the icon, screenshots, everythig a player sees about your game is marketing.
Icons and bylines are especially important, since they're the first things your players will probably see.
I saw an almost 50% (!) by changing the icon, after seveeral experiments, which Google Play lets you conduct very easily.
## Translations are part of your source code
This may be slightly contraversial, so I'll explain.
We went though a number of iterations regarding how to save translations until we arrived at the current format.
The important parts are:
- Game translation files should be AUTO GENERATED. This allows you to add new objects into the game with impunity,
knowing that corresponding lines will be auto-added to the translations.
- Translations for each language should be stored separately - this allows concurrent modification of several independant languages with no risk of conflict
- Translations should be PR'd in! This allows other speakers to question or change the proposed translations, and allows you to run tests on your translations.
If you require a specific format, this is invaluable as it means that bad translations will be rejected at the door.
## Open source problems require open (source?) solutions
TL;DR, consider using APIs that are free, even if they're not Open Source.
Multiplayer requires syncing game files beween clients, even when one of them is not currently online.
The 'correct' way to solve this would probably be to have an online DB and a service which handles user requests.
Since this is an Open Source game, I'm working on a 0$ budget, so we just store all the files in Dropbox and upload/download there.
Is this secure? No, but does it need to be? You need to think of the cost vs the value.
Same thing with Mods. Steam is big and secure so it handles its mods itself.
We are small and open, so we just allow to download from Github, which lets us use all of Github's built in functions (user management, readmes, stars, versioning...) at no extra cost.
And unlike the Dropbox usage, which is basically abuse, Github is built for thiss kind of thing!
This is exactly the kind of use case they were thinking of to start with!
# The Reckoning
There comes a time in every project where the cool stuff is done. All the cutting-edge awesomeness and algorithmic playdough is done, and now all (hah) it needs is polish.
You know who loves polish? Players! Sure, there are some that say "a good game is good even if it's basic" but they have *standards* for what a basic game should have as well.
And the numbers don't lie. Polished games sell themselves better, and so are played more.
You know who **doesn't** love polish? DEVELOPERS.
When your game is relatively simple, then the options for polish are more limited, but the more complex the game, the more polish-venues there are.
And it can be an ABSOLUTE GRIND. Another weird use-case, another ingame option, "better performance" (I must have spent dozens of hours on different performance related actions)
And the worst thing is, that everyone notices when it's missing, but no one notices when it's there. A hundred versions of polish - literally - and the average player may notice only a slight change.
And then comes the moment when you ask yourself, why bother? What are we even doing here?
For me, the answers are as follows:
A. To build something truly great, you have to keep going way beyond when it stops being fun.
B. There's a community of people that like what you're doing and want there to be more of it :)
C. You know you want to keep coding, and what, you think you're going to start another project and it'll work out as well? You've tried that multiple times, and let's face it the chance of you making a second game that goes so well is really small unless you invest in it as much time as you have in this, and yeah, then you'll be back in this position again.
And that's basically the loop I've been in for the last hundred versions or so! Solve bugs, fix edge cases, improve AI, accept PRs. Lots of mod-related changes, both to stop the game breaking when people do things in mods that they shouldn't and to allow them more freedom in making them.
I don't think I'll ever really continue to finish G&K, I'm DEFINITELY not planning on implementing BNW mechanics which frankly I think are...not great.
That's where I am right now. Kind of done with the game, but considering that I thought that half a year ago and releases are still releasing roughly every week, also kind of not.

View File

@ -0,0 +1,29 @@
This is a guide to editing, building, running and deploying Unciv from code
So first things first - the initial "No assumptions" setup to have Unciv run from-code on your computer!
* Install Android Studio - it's free and awesome! Be aware that it's a long download!
* Install Git, it's the way for us to work together on this project. UI is optional, Android Studio has good Git tools built in :)
* Getting the code
* Create a Github account, if you don't already have one
* Fork the repo (click the "Fork" button on the top-right corner of https://github.com/yairm210/Unciv) - this will create a "copy" of the code on your account, at https://github.com/YourUsername/Unciv
* Clone your fork with git - the location will be https://github.com/YourUsername/Unciv.git, visible from the green "Clone or download" button at https://github.com/YourUsername/Unciv
* Load the project in Android Studio, Gradle will attempt the initial sync. If this is your first time with Android Studio, this may require you to accept the Android Build-tools licenses, which works differently on every device, so search for your OS-specific solution.
* A new install may not be able to do the initial sync - this comes in the form of `Unable to find method ''void org.apache.commons.compress.archivers.zip.ZipFile.<init>(java.nio.channels.SeekableByteChannel)''` errors when you try to sync. If you have this problem go into File > Settings > Appearance & Behavior > System Settings > Android SDK
* Click "SDK Tools"
* Select "Show Package Details" in the bottom right
* Choose version 30.0.3 under "Android SDK Build-Tools <whatever version you have>"
* Click "Apply"
* In Android Studio, Run > Edit configurations.
* Click "+" to add a new configuration
* Choose "Application"
* Set the module to `Unciv.desktop`, main class to `com.unciv.app.desktop.DesktopLauncher` and `<repo_folder>\android\assets\` as the Working directory, OK to close the window
* If you get a `../../docs/uniques.md (No such file or directory)` error that means you forgot to set the working directory!
* Select the Desktop configuration and click the green arrow button to run!
* I also recommend going to Settings > Version Control > Commit and turning off 'Before commit - perform code analysis'
Unciv uses Gradle to specify dependencies and how to run. In the background, the Gradle gnomes will be off fetching the packages (a one-time effort) and, once that's done, will build the project!
Congratulations! Unciv should now be running on your computer! Now we can start changing some code, and later we'll see how your changes make it into the main repository!
Now would be a good time to get to know the project in general at [the Project Structure overview!](Project-structure-and-major-classes.md)

View File

@ -0,0 +1,179 @@
# Project structure
Since LibGDX, and therefore Unciv, are built for multi-platform support, the project structure is built accordingly.
99% of the code is in the [Core](/com/unciv) project, which contains all the platform-independant code.
The [Desktop](/) and [Android](/) folders contain platform-specific things, and the Android folder also contains the game Images and the all-important Assets, which are required for running from Desktop as well, so we bundle them up into the .jar file when releasing.
The [Test](/com/unciv) folder contains tests that can be run manually via gradle with `./gradlew tests:test`, and are run automatically by Travis for every push.
# Translations
Before we get to the Classes, a word on Languages. Unciv is playable in several handfuls of languages, and there's magic to support that. Whenever you include a new string in code you will need to give it a quick evaluation - will users see it, and if so, what do I need to do to support its translations. Sometimes you may not need to do anything, sometimes you will add a line to the [translation templates](/jsons/translations/template.properties), and sometimes you will adapt the string formatting to support the translations. For details, see [the 'Translation generation - for developers' chapter](../Other/Translating.md#translation-generation---for-developers).
# Major classes
Civ, and therefore Unciv, is a game with endless interconnectivity - everything affects everything else.
In order to have some semblance of order, we'll go over the main classes in the order in which they are serialized.
So yes, you can - for instance - get the center tile of a city, a TileInfo, directly from CityInfo. But delving into all the connections would only harm the point of this overview, that's what the actual code is for ;)
The Game State:
* GameInfo
* CivilizationInfo
* CityInfo
* TileMap
* TileInfo
* MapUnit
* RuleSet (unique in that it is not part of the game state)
The UI:
* MainMenuScreen
* NewGameScreen
* WorldScreen
* CityScreen
* MapEditorScreen
* Picker Screens - TechPickerScreen, PolicyPickerScreen, ImprovementPickerScreen, PromotionPickerScreen
# Game State
## The Game - `GameInfo`
First off, let's clarify: When we say "The Game", we mean the *state* of the game (what turn it is, who the players are, what each one has etc) and not the *UI* of the game.
That is, The Game is the *currently played* game, not *Unciv*.
The game contains three major parts:
- The list of the players, or civilizations - `List<CivilizationInfo>`
- The map upon which the game is played - `TileMap`
- The ruleset by which the game is played - `RuleSet`. This includes what technologies, buildings, units etc. are available, and IS NOT serialized and deserialized, but comes straight from the game files - more on that later.
- Parameters unique to this game - difficulty, game speed, victory conditions, etc.
When we save the game, or load the game, we're actually serializing and deserializing this class, which means that the this class is the root of the entire game state.
Most objects in the "state tree" have a transient reference to their parent, meaning the tree can be traversed in-code in all directions, and frequently is.
## A Civilization - `CivilizationInfo`
This represents one of the players of the game, and NOT a specific nation - meaning, not France, but rather "Player X who is France in this game". In another game, there will be another France.
As one of the focal points of the game, it contains a lot of important information, the most important of which are:
- The list of cities the civilization has - `List<CityInfo>`
- Which nation this is - references a certain Nation (part of the ruleset)
- Various Managers for the different aspects of the civilization - `PolicyManager`, `GoldenAgeManager`, `GreatPersonManager`, `TechManager`, `VictoryManager`, `DiplomacyManager`
## A City - `CityInfo`
This contains the information about a specific city.
Beyond basic information like name, location on map etc, the most important classes it contains are:
- Calculating the yield of the city - `CityStats`
- Managers for the various aspects - `PopulationManager`, `CityConstructions`, `CityExpansionManager`
- The tiles controlled and worked by the city - only their locations are permanently saved in the CityInfo, the actual information is in the TileInfo in the TileMap
## The map - `TileMap`
This contains mostly helper functions and acts as a wrapper for the list of tiles it contains
## A tile - `TileInfo`
Each tile is comprised of several layers, and so has information for each.
Tiles have, primarily:
- A base terrain - Grassland, Hills, Desert etc. References a certain `Terrain` (part of the ruleset)
- An optional terrain feature - Forest, Jungle, Oasis etc. References a certain `Terrain` (part of the ruleset)
- An optional resource - Iron, Dye, Wheat etc. References a certain `TileResource` (part of the ruleset)
- An improvement built on the tile, if any. References a certain `TileImprovement` (part of the ruleset)
- The units that are currently in the tile - `MapUnit`
## A unit on the map - `MapUnit`
Unlike buildings, Unit in Unciv has two meanings. One is a *Type* of unit (like Spearman), and one is a specific instance of a unit (say, a Babylonian Spearman, at a certain position, with X health).
`MapUnit` is a specific instance of a unit, whereas `BaseUnit` is the type of unit.
Main information:
- A name - references a specific `BaseUnit`
- Health and Movement
- Promotion status - `UnitPromotions`
## Ruleset
So far so good - but what of everything that makes Civ, Civ? The units, the buildings, the nations, the improvements etc?
Since these things remain the same for every game, these are not saved on a per-game basis, but rather are saved in json files in Unciv's asset folder.
Each class in the game state that saves one of these will reference it by name, and when the game is running it will check the Ruleset to find the relevant information for that object.
The various objects are:
- `Technology` - referenced mainly in `CivilizationInfo.TechManager`
- `Nations` - referenced mainly in `CivilizationInfo`
- `Policy` - referenced mainly in `CivilizationInfo.PolicyManager` (seeing a pattern here?)
- `Building` - referenced mainly in `CityInfo.ConstructionManager`
- `BaseUnit` - referenced mainly in `MapUnit`
- `Promotion` - referenced mainly in `MapUnit`
- `Terrain` - referenced mainly in `TileInfo`
- `TileResource` - referenced mainly in `TileInfo`
- `TileImprovement` - referenced mainly in `TileInfo`
There are also Translations in the Ruleset, but they technically have nothing to do with the game state but rather with the UI display.
The information for all of these is in json files in `android\assets\jsons`
# UI
`UncivGame` is the 'base' class for the UI, from which everything starts, but it itself doesn't do much.
When we change a screen, we're changing a value in UncivGame, the interesting stuff happens in the screens themselves.
## The main menu - `MainMenuScreen`
This is what the user sees when first entering the game. It acts as a hub to loading games, adding mods, options etc, without loading an actual game upfront - this allows us to differentiate between "User can't enter game" and "User can't load game" problems
## Starting a new game - `NewGameScreen`
This is basically a giant setting screen for GameOptions and MapOptions classes, divided into:
* GameOptionsTable - game speed, mods, etc
* MapOptionsTable - either from preexisting map file or generated, in which case: size, map generation type, etc.
* PlayerPickerTable - What civs are in the game and who controls them
## The World Screen - `WorldScreen`
90% of the game is spent on this screen, so naturally it's the fullest, with the most things happening.
This is the main hub of the game, with all other screens being opened from it, and closing back to reveal it.
Most notable are:
* The map itself - a `TileMapHolder` - with each of the rendered tiles being a `TileGroup`
* The information panels - `WorldScreenTopBar` for stats and resources, `UnitTable` for the currently selected unit, `TileInfoTable` or the currently selected tile, `BattleTable` for battle simulation, and `NotificationsScroll` for the notifications
* The minimap - `MinimapHolder`
* Buttons linking to other screens - to the `TechPickerScreen`, `EmpireOverviewScreen`, and `PolicyPickerScreen`
* The almighty Next Turn button
## The city screen - `CityScreen`
The second-most important screen.
Notable parts:
* the City Stats table - should definitely be its own class come to think of it
* The construction list and current construction (bottom left) - `ConstructionsTable`
* Existing buildings, specialists and stats drilldown - `CityInfoTable`
# Others
A few words need to be said about the NextTurn process, but there isn't really a good place for it so I'll put it here.
We clone the GameInfo and use a "new" GameInfo for each turn because of 2 reasons.
The first is multithreading and thread safety, and the second is multiplayer reproducibility.
The first point is pretty basic. The NextTurn needs to happen in a separate thread so that the user can still have a responsive game when it's off doing stuff. Stuff in the GameInfo changes on NextTurn, so if you're rendering that same GameInfo, this could cause conflicts. Also, after NextTurn we generally autosave, and if stuff changes in the state while we're trying to serialize it to put it in the save file, that's Not Fun. A single clone solves both of these problems at once.
The second point is less obvious. If we use our mutable state, changing stuff in place, then what happens when we're playing in Multiplayer? Multiplayer is based upon the fact that you can receive an entire game state and go from there, and in fact the move to multiplayer was what made the whole "clone" thing necessary (on the way it also solved the aforementioned threading problems)

View File

@ -0,0 +1,137 @@
Unciv is, at its core, a remake of Civ V, meaning mechanics-wise there's almost by definition not much place for innovation.
In terms of UI, there's nothing here that hasn't been done dozens of times, with far greater polish.
However, there is one area where Unciv is groundbreaking: in its accessibility of translations, the possibility space of its mods, and the relationship between them.
# Translations
## The translation process
So let's start with translation. Surely this is a solved problem, right? Source text + language = translated text, and this information needs to be in a file so the game can read it. What makes us different from, for example, Firaxis?
There are a couple of things, but the most significant is that this is an open-source game, and thus the *translations* are open-source as well.
This means translators are both amateurs and not *obligated* to translate, so if translating is difficult, they simply won't.
Amateurs can make mistakes, which is why it's vital that mistakes are easy to spot. That means that formats like "translation key" - e.g. `DIPLOMACY_GREETING = Siamo lieti di fare la vostra conoscenza.` are much less effective than `A pleasure to meet you. = Siamo lieti di fare la vostra conoscenza.` This format lends itself both the easier translation (it's immediately obvious what needs to be translated) and actual collaboration.
A common suggestion that we get (by people with little familiarity with the project) is to "use a website for translation". This is not bad advice for a small open source game, but there are multiple disadvantages that (for now) no translation website provides enough advantage to outweigh:
1. **Testing**. Currently, translations undergo a number of tests for verification - more on that later! This allows some language changes to be accepted and others not, and it's all in the same platform with the same tests. External translation tools don't allow for this.
2. **History and revisions**. This is what Git was made for, and nothing like it exists in the world. By itself this would not
3. **Release cycle**. We release versions semiweekly, and if we needed to upload changes to the translation website for every in-game change, and download them for every release, that's extra work. For some websites this is automate-able - for most it is not.
4. **Discussions**. Most crowdsourcing translation websites don't allow for discussions and corrections on translations. Github makes every translation collaborative work.
5. **Mass changes**. If we're changing the source of the translation but want to keep the various destinations (say, we change "Gold from trade routes +[amount]%" to "+[amount]% Gold from trade routes"), if all the translation files are in Git we can do that in 1 minute. If it's external, this varies greatly.
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.
## Building the translation files
As stated, Unciv releases versions semiweekly, and very often these changes include new objects or new UI elements. How do we keep all translation files up to date?
In Unciv, all object data is stored in json format. This allows us to iterate on all objects, regardless of type, and extract the various text fields (strings or lists of strings). We avoid duplication by saving all translation texts we've already added, and use the *existing* translations to populate the "value" for each translation "key" we found in the json files.
Since we rebuild the entire translation file every time, there's currently no way for translators to retain their own comments for future translators.
But on the other hand, since for each line that we add we already know if it's translated or not, this allows us to add a `# Requires translation` line before every non-translated line, which helps translators for languages that are almost fully translated to easily locate the new or changed terms for translation with ctrl+f (and of course this marking will disappear the next time we rebuild the file).
Since there are UI texts that are not part of any specific object (like "Start new game"), we have a separate template.properties file for texts to translate that are not in the json files. Unlike adding objects, where the developer doesn't need to address the translation files at all since it's all linked, when adding UI elements with new texts devs need to remember to add the texts to template.properties file.
## Translation placeholders
This is all well and good for specific text-to-text translations, but what about translating "A Temple has been built in Rome"? The same template could potentially be any building name, or any city name!
We do this with placeholders, which looks something like this: `[construction] has been built in [cityName] = [cityName] ha costruito [construction]`.
As you can see, the *placement* of the parameters can change between languages, so we have to name all parameters.
This also means that there can be explicitly *wrong* translations - if any parameter that appears in the source does not appear in the translated version, we won't be able to display this in-game! This is one of the translation tests that we mentioned earlier - when a translator opens a PR, the game undergoes build & test via the Github Actions, and will notify on failures. Finding the text that warns of the failure within the action output is currently mostly done by devs, but I hope to be able to automate this too someday.
To translate a text like "[Temple] has been built in [Rome]", therefore, we need to:
- Find the relevant translation (we do this by erasing all text between square brackets in input and finding the relevant translation text)
- Map placeholder names to input text (construction = Temple, cityName = Rome)
- Replace placeholders in translation with TRANSLATED input text (in `[cityName] ha costruito [construction]`, replace "[cityName]" with translation of "Rome", and "[construction]" with translation of "Temple")
## Translating mod data
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-parameter-types.md).
## 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.