diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index db2fdbd1..0eac1c20 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,7 +7,8 @@ assignees: luskaner --- -**â„šī¸ MAKE SURE YOU HAVE READ [TROUBLESHOOTING](https://github.com/luskaner/ageLANServer/wiki/Troubleshooting) and [QA](https://github.com/luskaner/ageLANServer/wiki/Questions-and-Answers-(QA)) FIRST**. +**â„šī¸ MAKE SURE YOU HAVE READ [TROUBLESHOOTING](https://github.com/luskaner/ageLANServer/wiki/Troubleshooting) +and [QA](https://github.com/luskaner/ageLANServer/wiki/Questions-and-Answers-(QA)) FIRST**. You are expected to do a followup on any doubts or testing required to reproduce or resolve the issue. @@ -41,6 +42,7 @@ Check all that apply: - [ ] Age of Empires: Definitive Edition - [ ] Age of Empires II: Definitive Edition - [ ] Age of Empires III: Definitive Edition +- [ ] Age of Mythology: Retold **Server** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a2fb57a9..4349d61c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,8 +8,11 @@ updates: - package-ecosystem: "gomod" # See documentation for possible values directories: # Location of package manifests - "/common" + - "/battle-server-*" - "/launcher*" - "/server" - "/server-genCert" + - "/tools/scripts" + - "/tools/server-replay" schedule: interval: "daily" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4d6e7204..837581ef 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -88,7 +88,12 @@ jobs: go build -o build/launcher/bin/config.exe ./launcher-config go build -o build/launcher/bin/config-admin.exe ./launcher-config-admin go build -o build/launcher/bin/config-admin-agent.exe ./launcher-config-admin-agent - + go build -o build/battle-server-manager/battle-server-manager.exe ./battle-server-manager + go build -o build/tools/server-replay.exe ./tools/server-replay + go build -o build/tools/scripts/copyBattleServerManagerResources.exe ./tools/scripts/cmd/copyBattleServerManagerResources.go + go build -o build/tools/scripts/copyLauncherResources.exe ./tools/scripts/cmd/copyLauncherResources.go + go build -o build/tools/scripts/copyServerResources.exe ./tools/scripts/cmd/copyServerResources.go + go build -o build/tools/scripts/createServerResourcesFolder.exe ./tools/scripts/cmd/createServerResourcesFolder.go - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 0bacd254..9af7cbca 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -13,18 +13,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Rename go.work shell: bash run: | cp go.work.example go.work - - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: '~1.24.0' + go-version: '1.25.5' cache-dependency-path: | common/go.sum server/go.sum @@ -35,14 +34,17 @@ jobs: launcher-config/go.sum launcher-config-admin/go.sum launcher-config-admin-agent/go.sum - + battle-server-manager/go.sum + - name: Generate Goreleaser config + shell: bash + run: | + go run tools/scripts/cmd/generateGoreleaserConfig.go - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} - - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/.gitignore b/.gitignore index f6c01f24..bc024d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ go.work go.work.sum +.tools +.task .env .idea/ dist/ @@ -10,4 +12,5 @@ build/**/* !build/launcher/ !build/launcher/.gitkeep !build/battle-server-manager/ -!build/battle-server-manager/.gitkeep \ No newline at end of file +!build/battle-server-manager/.gitkeep +.goreleaser.yaml \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 92242032..56b840cf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -329,7 +329,7 @@ archives: - launcher-config-admin_full_windows_amd64 - launcher-config-admin-agent_full_windows_amd64 - battle-server-manager_full_windows_amd64 - name_template: "{{ .ProjectName }}_full_{{ .RawVersion }}_win_x86-64" + name_template: "{{ .ProjectName }}_full_{{ .RawVersion }}_win10_x86-64" files: - src: LICENSE dst: docs/LICENSE.txt @@ -403,7 +403,7 @@ archives: - launcher-config-admin_full_windows_arm64 - launcher-config-admin-agent_full_windows_arm64 - battle-server-manager_full_windows_arm64 - name_template: "{{ .ProjectName }}_full_{{ .RawVersion }}_win_arm64" + name_template: "{{ .ProjectName }}_full_{{ .RawVersion }}_win11_arm64" files: - src: LICENSE dst: docs/LICENSE.txt @@ -466,7 +466,7 @@ archives: dst: battle-server-manager/resources/config.athens.toml - src: battle-server-manager/README.md dst: battle-server-manager/docs/README.txt - formats: tar.xz + formats: zip - id: all_linux ids: - server_64_full_linux @@ -577,7 +577,7 @@ archives: dst: battle-server-manager/resources/config.athens.toml - src: battle-server-manager/README.md dst: battle-server-manager/docs/README - formats: tar.xz + formats: tar.gz - id: server_windows ids: - server_32_windows @@ -588,7 +588,7 @@ archives: - server-genCert_64_windows_arm64 name_template: >- {{- .ProjectName }}_server_ - {{- .RawVersion }}_win + {{- .RawVersion }}_win10 {{- if eq .Arch "386" }}_x86-32 {{- else if eq .Arch "amd64" }}_x86-64 {{- else if eq .Arch "arm64" }}_arm64 @@ -647,7 +647,7 @@ archives: dst: docs/README - src: server/BattleServers.md dst: docs/BattleServers.txt - formats: tar.xz + formats: tar.gz - id: server_linux_tar_gz ids: [ server_32_linux, server-genCert_32_linux, server_64_x86_linux, server-genCert_64_x86_linux ] name_template: >- @@ -689,6 +689,7 @@ archives: dst: docs/README - src: server/BattleServers.md dst: docs/BattleServers + formats: tar.gz - id: server_macos ids: [ server_64_macos, server-genCert_64_macos ] name_template: >- @@ -725,6 +726,7 @@ archives: dst: docs/README - src: server/BattleServers.md dst: docs/BattleServers + formats: tar.gz - id: launcher_windows_amd64 ids: - launcher_windows_amd64 @@ -757,7 +759,7 @@ archives: dst: start_age3.bat - src: launcher/resources/windows/start_athens.bat dst: start_athens.bat - name_template: "{{ .ProjectName }}_launcher_{{ .RawVersion }}_win_x86-64" + name_template: "{{ .ProjectName }}_launcher_{{ .RawVersion }}_win10_x86-64" formats: zip - id: launcher_windows_arm64 ids: @@ -791,8 +793,8 @@ archives: dst: start_age3.bat - src: launcher/resources/windows/start_athens.bat dst: start_athens.bat - name_template: "{{ .ProjectName }}_launcher_{{ .RawVersion }}_win_arm64" - formats: tar.xz + name_template: "{{ .ProjectName }}_launcher_{{ .RawVersion }}_win11_arm64" + formats: zip - id: launcher_linux ids: - launcher_linux @@ -839,17 +841,39 @@ archives: {{- if eq .Arch "amd64" }}_x86-64 {{- else if eq .Arch "arm64" }}_arm64 {{- end }} - formats: tar.xz - - id: battle-server-manager_windows + formats: tar.gz + - id: battle-server-manager_windows_amd64 ids: - battle-server-manager_windows_amd64 + name_template: "{{ .ProjectName }}_battle-server-manager_{{ .RawVersion }}_win10_x86-64" + files: + - src: battle-server-manager/resources/windows/clean.bat + dst: clean.bat + - src: battle-server-manager/resources/windows/remove-all.bat + dst: remove-all.bat + - src: battle-server-manager/resources/windows/start_age1.bat + dst: start_age1.bat + - src: battle-server-manager/resources/windows/start_age2.bat + dst: start_age2.bat + - src: battle-server-manager/resources/windows/start_age3.bat + dst: start_age3.bat + - src: battle-server-manager/resources/windows/start_athens.bat + dst: start_athens.bat + - src: battle-server-manager/resources/config.game.toml + dst: resources/config.age1.toml + - src: battle-server-manager/resources/config.game.toml + dst: resources/config.age2.toml + - src: battle-server-manager/resources/config.game.toml + dst: resources/config.age3.toml + - src: battle-server-manager/resources/config.game.toml + dst: resources/config.athens.toml + - src: README.md + dst: docs/README.txt + formats: zip + - id: battle-server-manager_windows_arm64 + ids: - battle-server-manager_windows_arm64 - name_template: >- - {{- .ProjectName }}_battle-server-manager_ - {{- .RawVersion }}_win - {{- if eq .Arch "amd64" }}_x86-64 - {{- else }}_arm64 - {{- end }} + name_template: "{{ .ProjectName }}_battle-server-manager_{{ .RawVersion }}_win11_arm64" files: - src: battle-server-manager/resources/windows/clean.bat dst: clean.bat @@ -918,7 +942,7 @@ archives: dst: resources/config.athens.toml - src: README.md dst: docs/README - formats: tar.xz + formats: tar.gz universal_binaries: - id: server_64_macos name_template: 'server' diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1cc7fe67..59e18031 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -5,8 +5,10 @@ Copy `go.work.example` to `go.work` ### System requirements - OS requirements correspond to the server/launcher ones. Cross-compilation works on all systems out-the-box. -- [Go 1.24](https://go.dev/dl/). -- [Git](https://git-scm.com/downloads). +- [Go 1.25](https://go.dev/dl/) or higher, except for Windows 7-8 (and equivalent) which need an unofficial fork + like [thongtech/go-legacy-win7](https://github.com/thongtech/go-legacy-win7) (recommended) + or [XTLS/go-win7](https://github.com/XTLS/go-win7). +- [Git](https://git-scm.com/downloads), with the latest supported for Windows 7/8 being v2.46.2. - [Task](https://taskfile.dev/installation/). - [GoReleaser](https://goreleaser.com/). @@ -18,12 +20,12 @@ the [Go extension](https://marketplace.visualstudio.com/items?itemName=golang.go Depending on the module you want to debug, you will need to run the corresponding task **before**: -- server: ```task debug-prepare-server``` +- server: ```task debug:prepare-server``` - genCert: ```task debug:prepare-server-genCert``` - launcher: ```task debug:prepare-launcher``` - - config: ```task build-config-admin-agent``` - - config-admin-agent: ```task build-config-admin``` - - agent: ```task build-config-all``` + - config: ```task debug:build-config-admin-agent``` + - config-admin-agent: ```task debug:build-config-admin``` + - agent: ```task debug:build-config-all``` - battle-server-manager: ```task debug:prepare-battle-server-manager``` ### Build diff --git a/README.md b/README.md index 70d2c8c0..706214b8 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,7 @@ is in maintenance or is eventually shutdown. List of features - đŸ—Ŗī¸ Whispering. -- 🙏 Arena of the Gods: - * Story. - * Daily Celestial Challenge. +- 🙏 Arena of the Gods. @@ -107,10 +105,11 @@ is in maintenance or is eventually shutdown. - âš ī¸ **Friend list** will instead show all online users as if they were friends. - â„šī¸ **Arena of Gods**: - * âš ī¸ Challenge mode is [currently](https://github.com/luskaner/ageLANServer/issues/243) not supported. * No rewards are gained, **all blessings and rarities are unlocked by default**, including those not available in the official server. - * Story mode has all missiones unlocked by default. + * Favor stash is infinite. + * Story mode has all missions unlocked by default. + * Challenge mode lifes are infinite, you have access to all legends and you are max level (99) by default. @@ -125,9 +124,7 @@ is in maintenance or is eventually shutdown. - ❌ **Achievements**: only the official server should be able to. Meeting the requirements of an achievement during a match might cause issues (see [Troubleshooting](https://github.com/luskaner/ageLANServer/wiki/Troubleshooting) for more details). -- ❌ Changing **player profile icon**: the default, or empty, will always be used. - ❌ **Leaderboards**: will appear empty. -- ❌ **Player stats**: will appear empty. - ❌ **Clans**: all players are outside clans. Browsing clan will appear empty and creating one will always result in error. - ❌ **Lobby ban player**: will appear like it works but doesn't. @@ -143,9 +140,9 @@ is in maintenance or is eventually shutdown. #### Stable -- **Windows**: 10 (or equivalent, not Arm32). +- **Windows**: 7 (or equivalent). - **Linux**: kernel 3.2 (see [here](https://go.dev/wiki/Linux) for more details). -- **macOS**: Big Sur (v11). +- **macOS**: Monterey (v12). Admin rights or firewall permission to listen on port 443 (https) will likely be required depending on the operating system and configuration. @@ -157,14 +154,14 @@ system and configuration. - Solaris-based (Solaris and Illumos). - AIX. -Note: For the full list see [minimum requirements for Go](https://go.dev/wiki/MinimumRequirements) 1.24. +Note: For the full list see [minimum requirements for Go](https://go.dev/wiki/MinimumRequirements) 1.25. ### Launcher and Battle Server Manager - Windows without S edition/mode (recommended): - - 10 on x86-64 (recommended). + - 7 on x86-64 (10 or higher recommended). - 11 on ARM. - Linux with kernel 3.2: - x86-64 (recommended). @@ -178,16 +175,16 @@ it will require admin rights elevation.** - Age of Empires: Definitive Edition on [Steam](https://store.steampowered.com/app/1017900/Age_of_Empires_Definitive_Edition) - or [Xbox](https://www.xbox.com/games/store/age-of-empires-definitive-edition/9njwtjsvgvlj) (*only on - Windows*). Recommended version *100.2.31845.0* or later. + or [Xbox](https://www.xbox.com/games/store/age-of-empires-definitive-edition/9njwtjsvgvlj) (*only on a compatible + Windows version*). Recommended version *100.2.31845.0* or later. - Age of Empires II: Definitive Edition on [Steam](https://store.steampowered.com/app/813780/Age_of_Empires_II_Definitive_Edition) - or [Xbox](https://www.xbox.com/games/store/age-of-empires-ii-definitive-edition/9N42SSSX2MTG/0010) (*only on - Windows*). Recommended a late 2023 version or later. + or [Xbox](https://www.xbox.com/games/store/age-of-empires-ii-definitive-edition/9N42SSSX2MTG/0010) (*only on a + compatible Windows version*). Recommended a late 2023 version or later. - Age of Empires III: Definitive Edition on [Steam](https://store.steampowered.com/app/933110/Age_of_Empires_III_Definitive_Edition) - or [Xbox](https://www.xbox.com/games/store/age-of-empires-iii-definitive-edition/9n1hf804qxn4) (*only on - Windows*). Recommended a late 2023 version or later. + or [Xbox](https://www.xbox.com/games/store/age-of-empires-iii-definitive-edition/9n1hf804qxn4) (*only on a compatible + Windows version*). Recommended a late 2023 version or later. - Age of Mythology: Retold on [Steam](https://store.steampowered.com/app/1934680/Age_of_Mythology_Retold). Recommended a 2025 version or later. @@ -203,38 +200,49 @@ supported operating systems. * Full: * Windows: - * **10 on x86-64**: ...\_full\_*A.B.C*_win_x86-64.zip - * **11 on ARM**: ...\_full\_*A.B.C*_win_arm64.tar.xz + * **7 on x86-64**: ...\_full\_*A.B.C*_win7_x86-64.zip + * **10 on x86-64**: ...\_full\_*A.B.C*_win10_x86-64.zip + * **11 on ARM**: ...\_full\_*A.B.C*_win11_arm64.zip * Linux: - * **x86-64**: ...\_full\_*A.B.C*_linux_x86-64.tar.xz - * **ARM64**: ...\_full\_*A.B.C*_linux_arm64.tar.xz + * **x86-64**: ...\_full\_*A.B.C*_linux_x86-64.tar.gz + * **ARM64**: ...\_full\_*A.B.C*_linux_arm64.tar.gz * Launcher: * Windows: - * **10 on x86-64**: ...\_launcher\_*A.B.C*_win_x86-64.zip - * **11 on ARM**: ...\_launcher\_*A.B.C*_win_arm64.tar.xz + * **7 on x86-64**: ...\_launcher\_*A.B.C*_win7_x86-64.zip + * **10 on x86-64**: ...\_launcher\_*A.B.C*_win10_x86-64.zip + * **11 on ARM**: ...\_launcher\_*A.B.C*_win11_arm64.zip * Linux: - * **x86-64**: ...\_launcher\_*A.B.C*_linux_x86-64.tar.xz - * **ARM64**: ...\_launcher\_*A.B.C*_linux_arm64.tar.xz + * **x86-64**: ...\_launcher\_*A.B.C*_linux_x86-64.tar.gz + * **ARM64**: ...\_launcher\_*A.B.C*_linux_arm64.tar.gz * Battle Server Manager: * Windows: - * **10 on x86-64**: ...\_battle-server-manager\_*A.B.C*_win_x86-64.zip - * **11 on ARM**: ...\_battle-server-manager\_*A.B.C*_win_arm64.tar.xz + * **7 on x86-64**: ...\_battle-server-manager\_*A.B.C*_win7_x86-64.zip + * **10 on x86-64**: ...\_battle-server-manager\_*A.B.C*_win10_x86-64.zip + * **11 on ARM**: ...\_battle-server-manager\_*A.B.C*_win11_arm64.zip * Linux: - * **x86-64**: ...\_battle-server-manager\_*A.B.C*_linux_x86-64.tar.xz - * **ARM64**: ...\_battle-server-manager\_*A.B.C*_linux_arm64.tar.xz + * **x86-64**: ...\_battle-server-manager\_*A.B.C*_linux_x86-64.tar.gz + * **ARM64**: ...\_battle-server-manager\_*A.B.C*_linux_arm64.tar.gz * Server: * Windows: - * **10 (IoT), Server (IoT) 2025 on ARM64**: ...\_server\_*A.B.C*_win_arm64.zip - * **10 (IoT), (Storage) Server 2016, Server IoT 2019 on x86-64**: ...\_server\_*A.B.C*_win_x86-64.zip - * **10 (IoT) on x86-32**: ...\_server\_*A.B.C*_win_x86-32.zip + * **7, Server 2008 R2, Home Server 2011, Embedded 7 on x86-64**: + ...\_server\_ + *A.B.C*_ + win7_x86-64.zip + * **7, Embedded 7, Thin PC on x86-32**: + ...\_server\_ + *A.B.C*_ + win7_x86-32.zip + * **10 (IoT), Server (IoT) 2025 on ARM64**: ...\_server\_*A.B.C*_win10_arm64.zip + * **10 (IoT), (Storage) Server 2016, Server IoT 2019 on x86-64**: ...\_server\_*A.B.C*_win10_x86-64.zip + * **10 (IoT) on x86-32**: ...\_server\_*A.B.C*_win10_x86-32.zip * Linux: - * **ARM64**: ...\_server\_*A.B.C*_linux_arm64.tar.xz + * **ARM64**: ...\_server\_*A.B.C*_linux_arm64.tar.gz * **ARM32**: * ARMv5 (armel): ...\_server\_*A.B.C*_linux_arm-5.tar.gz * ARMv6 (sometimes called armhf): ...\_server\_*A.B.C*_linux_arm-6.tar.gz * **x86-64**: ...\_server\_*A.B.C*_linux_x86-64.tar.gz * **x86-32**: ...\_server\_*A.B.C*_linux_x86-32.tar.gz - * macOS - Big Sur (v11): ...\_server\_*A.B.C*_mac.tar.gz + * macOS - Monterey (v12): ...\_server\_*A.B.C*_mac.tar.gz diff --git a/Taskfile.debug.linux.yml b/Taskfile.debug.linux.yml new file mode 100644 index 00000000..28c8d778 --- /dev/null +++ b/Taskfile.debug.linux.yml @@ -0,0 +1,9 @@ +version: '3' + +tasks: + prepare-launcher: + platforms: [ linux ] + internal: true + deps: [ :debug:build-server ] + cmds: + - pkexec --keep-cwd setcap 'CAP_NET_BIND_SERVICE=+ep' build/server/server \ No newline at end of file diff --git a/Taskfile.debug.other.yml b/Taskfile.debug.other.yml new file mode 100644 index 00000000..d2ceda05 --- /dev/null +++ b/Taskfile.debug.other.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + # No-op + prepare-launcher: + internal: true \ No newline at end of file diff --git a/Taskfile.debug.yml b/Taskfile.debug.yml new file mode 100644 index 00000000..7a2e741e --- /dev/null +++ b/Taskfile.debug.yml @@ -0,0 +1,133 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj +version: '3' + +tasks: + # Start of prepare tasks + prepare-server-genCert: + sources: + - server/resources/**/* + generates: + - build/server/resources/**/* + method: checksum + cmds: + - task: :tools:run + vars: { PROGRAM: 'createServerResourcesFolder' } + - task: :tools:run + vars: { PROGRAM: 'copyServerResources' } + prepare-server: + deps: + - prepare-server-genCert + - build-genCert + sources: + - common/**/* + - server-genCert/**/* + generates: + - build/server/bin/genCert{{exeExt}} + method: checksum + cmds: + - build/server/bin/genCert{{exeExt}} -r + prepare-launcher: + deps: + - build-config-all + - prepare-server + - build-server + - build-launcher-agent + - os:prepare-launcher + sources: + - common/**/* + - launcher-common/**/* + - launcher/**/* + generates: + - build/launcher/resources/**/* + - build/launcher/launcher{{exeExt}} + method: checksum + cmds: + - task: :tools:run + vars: { PROGRAM: 'copyLauncherResources' } + prepare-battle-server-manager: + deps: [ build-battle-server-manager ] + sources: + - common/**/* + - battle-server-manager/**/* + generates: + - build/battle-server-manager/resources/**/* + method: checksum + cmds: + - task: :tools:run + vars: { PROGRAM: 'copyBattleServerManagerResources' } + build-server: + sources: + - common/**/* + - server/**/* + generates: + - build/server/server{{exeExt}} + method: checksum + cmds: + - go build -o build/server/server{{exeExt}} ./server + build-genCert: + sources: + - common/**/* + - server-genCert/**/* + generates: + - build/server/bin/genCert{{exeExt}} + method: checksum + cmds: + - go build -o build/server/bin/genCert ./server-genCert + # Start of build tasks + build-battle-server-manager: + sources: + - common/**/* + - battle-server-manager/**/* + generates: + - build/battle-server-manager/battle-server-manager{{exeExt}} + method: checksum + cmds: + - go build -o build/battle-server-manager/battle-server-manager{{exeExt}} ./battle-server-manager + build-config: + sources: + - common/**/* + - launcher-common/**/* + - launcher-config/**/* + generates: + - build/launcher/bin/config{{exeExt}} + method: checksum + cmds: + - go build -o build/launcher/bin/config{{exeExt}} ./launcher-config + build-launcher-agent: + deps: + - build-config-all + sources: + - common/**/* + - launcher-common/**/* + - launcher-agent/**/* + method: checksum + generates: + - build/launcher/bin/agent{{exeExt}} + cmds: + - go build -o build/launcher/bin/agent{{exeExt}} ./launcher-agent + build-config-admin: + sources: + - common/**/* + - launcher-common/**/* + - launcher-config-admin/**/* + generates: + - build/launcher/bin/config-admin{{exeExt}} + method: checksum + cmds: + - go build -o build/launcher/bin/config-admin{{exeExt}} ./launcher-config-admin + build-config-admin-agent: + deps: [ build-config-admin ] + sources: + - common/**/* + - launcher-common/**/* + - launcher-config-admin-agent/**/* + generates: + - build/launcher/bin/config-admin-agent{{exeExt}} + method: checksum + cmds: + - go build -o build/launcher/bin/config-admin-agent{{exeExt}} ./launcher-config-admin-agent + build-config-all: + deps: [ build-config, build-config-admin, build-config-admin-agent ] +includes: + os: ./Taskfile.debug.{{if eq OS "linux"}}linux{{else}}other{{end}}.yml \ No newline at end of file diff --git a/Taskfile.tools.yml b/Taskfile.tools.yml new file mode 100644 index 00000000..b4f402ec --- /dev/null +++ b/Taskfile.tools.yml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj +version: '3' + +tasks: + build: + label: "Build {{.PROGRAM}}{{exeExt}}" + internal: true + sources: + - tools/scripts/**/* + generates: + - .tools/{{.PROGRAM}}{{exeExt}} + method: checksum + cmds: + - go build -o ./.tools/{{.PROGRAM}}{{exeExt}} tools/scripts/cmd/{{.PROGRAM}}.go + copyBattleServerManagerResources: + internal: true + cmds: + - task: build + vars: { PROGRAM: 'copyBattleServerManagerResources' } + copyLauncherResources: + internal: true + cmds: + - task: build + vars: { PROGRAM: 'copyLauncherResources' } + createServerResourcesFolder: + internal: true + cmds: + - task: build + vars: { PROGRAM: 'createServerResourcesFolder' } + copyServerResources: + internal: true + cmds: + - task: build + vars: { PROGRAM: 'copyServerResources' } + run: + deps: + - task: "{{.PROGRAM}}" + cmds: + - ./.tools/{{.PROGRAM}}{{exeExt}} diff --git a/Taskfile.unix.yml b/Taskfile.unix.yml deleted file mode 100644 index e4676466..00000000 --- a/Taskfile.unix.yml +++ /dev/null @@ -1,37 +0,0 @@ -# yaml-language-server: $schema=https://taskfile.dev/schema.json -# vim: set ts=2 sw=2 tw=0 fo=cnqoj -version: '3' - -tasks: - prepare-server-base: - internal: true - cmds: - - cp -r server/resources build/server/resources - - go build -o build/server/bin/genCert ./server-genCert - prepare-launcher: - platforms: [ linux ] - deps: - - prepare-server-base - - :build-config-all - cmds: - - go build -o build/server ./server - - pkexec --keep-cwd setcap 'CAP_NET_BIND_SERVICE=+ep' build/server/server - - mkdir -p build/launcher/resources - - cp -f launcher/resources/config.toml build/launcher/resources - - cp -f launcher/resources/config.game.toml build/launcher/resources/config.age1.toml - - cp -f launcher/resources/config.game.toml build/launcher/resources/config.age2.toml - - cp -f launcher/resources/config.game.toml build/launcher/resources/config.age3.toml - - cp -f launcher/resources/config.game.toml build/launcher/resources/config.athens.toml - - go build -o build/launcher/bin/agent ./launcher-agent - prepare-server-genCert: - cmds: - - mkdir -p build/server/resources - prepare-battle-server-manager-base: - deps: - - :build-battle-server-manager - cmds: - - mkdir -p build/battle-server-manager/resources - - cp -f battle-server-manager/resources/config.game.toml build/battle-server-manager/resources/config.age1.toml - - cp -f battle-server-manager/resources/config.game.toml build/battle-server-manager/resources/config.age2.toml - - cp -f battle-server-manager/resources/config.game.toml build/battle-server-manager/resources/config.age3.toml - - cp -f battle-server-manager/resources/config.game.toml build/battle-server-manager/resources/config.athens.toml \ No newline at end of file diff --git a/Taskfile.windows.yml b/Taskfile.windows.yml deleted file mode 100644 index 4adbf591..00000000 --- a/Taskfile.windows.yml +++ /dev/null @@ -1,37 +0,0 @@ -# yaml-language-server: $schema=https://taskfile.dev/schema.json -# vim: set ts=2 sw=2 tw=0 fo=cnqoj -version: '3' - -tasks: - prepare-server-base: - internal: true - cmds: - - powershell New-Item -Path "build/server/resources" -ItemType Directory -ErrorAction Ignore -Force - - powershell Copy-Item -Path "server/resources/*" -Destination "build/server/resources" -Recurse -ErrorAction Ignore -Force - - go build -o build/server/bin/genCert.exe ./server-genCert - prepare-launcher: - deps: - - prepare-server-base - - :build-config-all - - prepare-battle-server-manager-base - cmds: - - go build -o build/server ./server - - powershell New-Item -ItemType Directory -Force -Path build/launcher/resources -ErrorAction Ignore - - powershell Copy-Item -Path launcher/resources/config.toml -Destination build/launcher/resources -ErrorAction Ignore -Force - - powershell Copy-Item -Path launcher/resources/config.game.toml -Destination build/launcher/resources/config.age1.toml -ErrorAction Ignore -Force - - powershell Copy-Item -Path launcher/resources/config.game.toml -Destination build/launcher/resources/config.age2.toml -ErrorAction Ignore -Force - - powershell Copy-Item -Path launcher/resources/config.game.toml -Destination build/launcher/resources/config.age3.toml -ErrorAction Ignore -Force - - powershell Copy-Item -Path launcher/resources/config.game.toml -Destination build/launcher/resources/config.athens.toml -ErrorAction Ignore -Force - - go build -o build/launcher/bin/agent.exe ./launcher-agent - prepare-server-genCert: - cmds: - - powershell New-Item -ItemType Directory -ErrorAction Ignore -Force -Path build/server/resources - prepare-battle-server-manager-base: - deps: - - :build-battle-server-manager - cmds: - - powershell New-Item -ItemType Directory -Force -Path build/battle-server-manager/resources -ErrorAction Ignore - - powershell Copy-Item -Path battle-server-manager/resources/config.game.toml -Destination build/battle-server-manager/resources/config.age1.toml -ErrorAction Ignore -Force - - powershell Copy-Item -Path battle-server-manager/resources/config.game.toml -Destination build/battle-server-manager/resources/config.age2.toml -ErrorAction Ignore -Force - - powershell Copy-Item -Path battle-server-manager/resources/config.game.toml -Destination build/battle-server-manager/resources/config.age3.toml -ErrorAction Ignore -Force - - powershell Copy-Item -Path battle-server-manager/resources/config.game.toml -Destination build/battle-server-manager/resources/config.athens.toml -ErrorAction Ignore -Force \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index e1b5655d..241b3a20 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,38 +5,25 @@ version: '3' dotenv: [ '.env', '{{.ENV}}/.env.', '{{.HOME}}/.env' ] tasks: + build-goreleaser-cfg: + internal: true + cmds: + - go run tools/scripts/cmd/generateGoreleaserConfig.go build: + deps: + - build-goreleaser-cfg cmds: - goreleaser build --clean --snapshot release: + deps: + - build-goreleaser-cfg cmds: - goreleaser release --clean --snapshot --fail-fast release-no-sign: + deps: + - build-goreleaser-cfg cmds: - goreleaser release --clean --snapshot --fail-fast --skip=sign - debug-prepare-server: - deps: [ debug:prepare-server-base ] - cmds: - - build/server/bin/genCert{{exeExt}} -r - prepare-battle-server-manager: - deps: [ debug:prepare-battle-server-manager-base, build-battle-server-manager, debug:prepare-server-base ] - cmds: - - go build -o build/battle-server-manager/battle-server-manager{{exeExt}} ./battle-server-manager - build-battle-server-manager: - internal: true - cmds: - - go build -o build/battle-server-manager/battle-server-manager{{exeExt}} ./battle-server-manager - build-config: - cmds: - - go build -o build/launcher/bin/config{{exeExt}} ./launcher-config - build-config-admin: - cmds: - - go build -o build/launcher/bin/config-admin{{exeExt}} ./launcher-config-admin - build-config-admin-agent: - deps: [ build-config-admin ] - cmds: - - go build -o build/launcher/bin/config-admin-agent{{exeExt}} ./launcher-config-admin-agent - build-config-all: - deps: [ build-config, build-config-admin-agent ] includes: - debug: ./Taskfile.{{if eq OS "windows"}}windows{{else}}unix{{end}}.yml \ No newline at end of file + debug: ./Taskfile.debug.yml + tools: ./Taskfile.tools.yml \ No newline at end of file diff --git a/battle-server-manager/README.md b/battle-server-manager/README.md index 38d63c1b..5fe1904f 100644 --- a/battle-server-manager/README.md +++ b/battle-server-manager/README.md @@ -6,10 +6,10 @@ Servers. ## Minimum system Requirements - Windows without S edition/mode (recommended): - - 10 on x86-64. + - 7 on x86-64 (10 or higher recommended). - 11 on ARM. - Linux with kernel 3.2: - - x86-64. + - x86-64 (recommended). - ARM64. ## Features diff --git a/battle-server-manager/go.mod b/battle-server-manager/go.mod index 79867ba6..e91a5b13 100644 --- a/battle-server-manager/go.mod +++ b/battle-server-manager/go.mod @@ -1,6 +1,6 @@ module battle-server-manager -go 1.24.0 +go 1.25.5 require ( github.com/deckarep/golang-set/v2 v2.8.0 @@ -10,10 +10,12 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -21,6 +23,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/battle-server-manager/go.sum b/battle-server-manager/go.sum index e1870be9..f8df0419 100644 --- a/battle-server-manager/go.sum +++ b/battle-server-manager/go.sum @@ -1,46 +1,25 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/battle-server-manager/internal/cmd/remove.go b/battle-server-manager/internal/cmd/remove.go index b949f0bb..4bb45307 100644 --- a/battle-server-manager/internal/cmd/remove.go +++ b/battle-server-manager/internal/cmd/remove.go @@ -10,9 +10,10 @@ import ( "github.com/luskaner/ageLANServer/common/cmd" "github.com/luskaner/ageLANServer/common/logger" "github.com/spf13/cobra" - "github.com/spf13/viper" ) +var region string + var RemoveCmd = &cobra.Command{ Use: "remove", Short: "remove will kill a given Battle Server instance and remove the config file", @@ -22,7 +23,6 @@ var RemoveCmd = &cobra.Command{ commonLogger.Println(err.Error()) os.Exit(internal.ErrGames) } - region := viper.GetString("Region") for gameId := range games.Iter() { commonLogger.Printf("Game: %s\n", gameId) commonLogger.Printf("\tRemoving '%s' region...\n", region) @@ -42,7 +42,8 @@ var RemoveCmd = &cobra.Command{ } func InitRemove() { - RemoveCmd.Flags().StringP( + RemoveCmd.Flags().StringVarP( + ®ion, "region", "r", "", @@ -53,8 +54,5 @@ func InitRemove() { if err != nil { panic(err) } - if err = viper.BindPFlag("Region", RemoveCmd.Flags().Lookup("region")); err != nil { - panic(err) - } RootCmd.AddCommand(RemoveCmd) } diff --git a/battle-server-manager/internal/cmd/root.go b/battle-server-manager/internal/cmd/root.go index 3652bf99..9cf756ed 100644 --- a/battle-server-manager/internal/cmd/root.go +++ b/battle-server-manager/internal/cmd/root.go @@ -5,8 +5,8 @@ import ( "path/filepath" "github.com/luskaner/ageLANServer/common" + "github.com/luskaner/ageLANServer/common/fileLock" "github.com/luskaner/ageLANServer/common/logger" - "github.com/luskaner/ageLANServer/common/pidLock" "github.com/spf13/cobra" ) @@ -19,7 +19,7 @@ var RootCmd = &cobra.Command{ var Version string func Execute() error { - lock := &pidLock.Lock{} + lock := &fileLock.PidLock{} if err := lock.Lock(); err != nil { commonLogger.Println("Failed to lock pid file. Kill process 'battle-server-manager' if it is running in your task manager.") commonLogger.Println(err.Error()) diff --git a/battle-server-manager/internal/cmd/start.go b/battle-server-manager/internal/cmd/start.go index c67b03de..baaf7169 100644 --- a/battle-server-manager/internal/cmd/start.go +++ b/battle-server-manager/internal/cmd/start.go @@ -3,6 +3,7 @@ package cmd import ( "battle-server-manager/internal" "battle-server-manager/internal/cmdUtils" + "errors" "fmt" "net" "os" @@ -13,12 +14,15 @@ import ( "github.com/luskaner/ageLANServer/common/cmd" commonExecutor "github.com/luskaner/ageLANServer/common/executor" "github.com/luskaner/ageLANServer/common/logger" + "github.com/luskaner/ageLANServer/common/paths" "github.com/luskaner/ageLANServer/common/process" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var configPaths = []string{"resources", "."} +var v = viper.New() + +var configPaths = []string{paths.ResourcesDir, "."} var hideWindow bool var gameId string var force bool @@ -32,7 +36,7 @@ var ( Short: "Run Battle Server instances.", Long: "Run Battle Server instances and setup configurations.", Run: func(_ *cobra.Command, _ []string) { - initConfig() + cfg := initConfig() gameIds := []string{gameId} games, err := cmdUtils.ParsedGameIds(&gameIds) if err != nil { @@ -45,8 +49,8 @@ var ( if isAdmin { commonLogger.Println("Running as administrator, this is not needed and might cause issues.") } - name := viper.GetString("Name") - region := viper.GetString("Region") + name := cfg.Name + region := cfg.Region err, names, regions := cmdUtils.ExistingServers(gameId) if err != nil { commonLogger.Printf("could not get existing servers: %s\n", err.Error()) @@ -86,7 +90,7 @@ var ( commonLogger.Printf("a Battle Server with the name/region '%s' already exists\n", region) os.Exit(internal.ErrAlreadyExists) } - host := viper.GetString("Host") + host := cfg.Host var ip string if host != "auto" { ips := common.HostOrIpToIps(host) @@ -109,11 +113,11 @@ var ( } else { ip = host } - bsPort := viper.GetInt("Ports.BsPort") - websocketPort := viper.GetInt("Ports.WebSocketPort") + bsPort := cfg.Ports.Bs + websocketPort := cfg.Ports.WebSocket outOfBandPort := -1 if gameId != common.GameAoE1 { - outOfBandPort = viper.GetInt("Ports.OutOfBandPort") + outOfBandPort = cfg.Ports.OutOfBand } if bsPort > 0 && !cmdUtils.Available(bsPort) { commonLogger.Printf("bs port %d is already in use\n", bsPort) @@ -143,18 +147,18 @@ var ( } resolvedCertFile, resolvedKeyFile, err := cmdUtils.ResolveSSLFilesPath( gameId, - viper.GetBool("SSL.Auto"), + cfg.SSL, ) if err != nil { commonLogger.Printf("could not resolve SSL files: %s\n", err) os.Exit(internal.ErrResolveSSLFiles) } - resolvedPath, err := cmdUtils.ResolvePath(gameId) + resolvedPath, err := cmdUtils.ResolvePath(gameId, cfg.Executable.Path) if err != nil { commonLogger.Printf("could not resolve path: %s\n", err) os.Exit(internal.ErrResolvePath) } - extraArgs, err := common.ParseCommandArgs("Executable.ExtraArgs", nil, true) + extraArgs, err := common.ParseCommandArgsFromSlice(cfg.Executable.ExtraArgs, nil, true) if err != nil { commonLogger.Printf("could not parse extra args: %s\n", err) os.Exit(internal.ErrParseArgs) @@ -227,25 +231,50 @@ func InitStart() { RootCmd.AddCommand(startCmd) } -func initConfig() { - viper.SetDefault("Region", "auto") - viper.SetDefault("Name", "auto") - viper.SetDefault("Host", "auto") - viper.SetDefault("Executable.Path", "auto") - viper.SetDefault("SSL.Auto", true) +func initConfig() *internal.Configuration { + // Defaults + v.SetDefault("Region", "auto") + v.SetDefault("Name", "auto") + v.SetDefault("Host", "auto") + // Executable + v.SetDefault("Executable.Path", "auto") + v.SetDefault("Executable.ExtraArgs", []string{}) + // Ports + v.SetDefault("Ports.Bs", 0) + v.SetDefault("Ports.WebSocket", 0) + v.SetDefault("Ports.OutOfBand", 0) + // SSL + v.SetDefault("SSL.Auto", true) + v.SetDefault("SSL.CertFile", "") + v.SetDefault("SSL.KeyFile", "") + for _, configPath := range configPaths { - viper.AddConfigPath(configPath) + v.AddConfigPath(configPath) } - viper.SetConfigType("toml") + v.SetConfigType("toml") if gameCfgFile != "" { - viper.SetConfigFile(gameCfgFile) + v.SetConfigFile(gameCfgFile) } else { - viper.SetConfigName(fmt.Sprintf("config.%s", gameId)) + v.SetConfigName(fmt.Sprintf("config.%s", gameId)) } - if err := viper.ReadInConfig(); err == nil { - commonLogger.Println("Using config file:", viper.ConfigFileUsed()) + if err := v.ReadInConfig(); err == nil { + commonLogger.Println("Using config file:", v.ConfigFileUsed()) + data, _ := os.ReadFile(v.ConfigFileUsed()) + commonLogger.PrefixPrintln("config", string(data)) } else { - commonLogger.Println("No config file found, using defaults.") + var configFileNotFoundError viper.ConfigFileNotFoundError + if errors.As(err, &configFileNotFoundError) { + commonLogger.Println("No config file found, using defaults.") + } else { + commonLogger.Println("Error parsing config file:", v.ConfigFileUsed()+":", err.Error()) + os.Exit(common.ErrConfigParse) + } + } + v.AutomaticEnv() + var c *internal.Configuration + if err := v.Unmarshal(&c); err != nil { + commonLogger.Printf("unable to decode configuration: %v\n", err) + os.Exit(common.ErrConfigParse) } - viper.AutomaticEnv() + return c } diff --git a/battle-server-manager/internal/cmdUtils/executor.go b/battle-server-manager/internal/cmdUtils/executor.go index 701d6844..ad9e05f4 100644 --- a/battle-server-manager/internal/cmdUtils/executor.go +++ b/battle-server-manager/internal/cmdUtils/executor.go @@ -10,10 +10,9 @@ import ( "github.com/luskaner/ageLANServer/common/game/appx" "github.com/luskaner/ageLANServer/common/game/steam" "github.com/luskaner/ageLANServer/common/logger" - "github.com/spf13/viper" ) -func ResolvePath(gameId string) (resolvedPath string, err error) { +func ResolvePath(gameId string, executablePath string) (resolvedPath string, err error) { validPath := func(path string) bool { if f, localErr := os.Stat(path); localErr == nil && !f.IsDir() { return true @@ -21,7 +20,7 @@ func ResolvePath(gameId string) (resolvedPath string, err error) { return false } var path string - if viper.GetString("Executable.Path") == "auto" { + if executablePath == "auto" { commonLogger.Println("Auto resolving executable path...") // TODO: Review if AoE: DE and AoE III: DE can also use AoE II: DE // The Battle Server for AoM is buggy and the only one working is the AoE II one @@ -53,13 +52,13 @@ func ResolvePath(gameId string) (resolvedPath string, err error) { } err = fmt.Errorf("could not find battle server executable") return - } else { - var pathErr error - _, path, pathErr = common.ParsePath(viper.GetStringSlice("Executable.Path"), nil) - if pathErr != nil { - err = fmt.Errorf("invalid battle server executable path") - return - } + } + + var pathErr error + _, path, pathErr = common.ParsePath(common.EnhancedViperStringToStringSlice(executablePath), nil) + if pathErr != nil { + err = fmt.Errorf("invalid battle server executable path") + return } if validPath(path) { resolvedPath = path diff --git a/battle-server-manager/internal/cmdUtils/gameId.go b/battle-server-manager/internal/cmdUtils/gameId.go index 77df6679..9d8ebbd8 100644 --- a/battle-server-manager/internal/cmdUtils/gameId.go +++ b/battle-server-manager/internal/cmdUtils/gameId.go @@ -18,8 +18,8 @@ func ParsedGameIds(gameIds *[]string) (games mapset.Set[string], err error) { } else if !common.SupportedGames.IsSuperset(mapset.NewThreadUnsafeSet[string](*gameIds...)) { err = fmt.Errorf("game(s) not supported") return - } else { - games = mapset.NewSet[string](*gameIds...) } + + games = mapset.NewSet[string](*gameIds...) return } diff --git a/battle-server-manager/internal/cmdUtils/ssl.go b/battle-server-manager/internal/cmdUtils/ssl.go index bc383ca3..d0c4908c 100644 --- a/battle-server-manager/internal/cmdUtils/ssl.go +++ b/battle-server-manager/internal/cmdUtils/ssl.go @@ -1,17 +1,17 @@ package cmdUtils import ( + "battle-server-manager/internal" "fmt" "os" "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/executables" "github.com/luskaner/ageLANServer/common/logger" - "github.com/spf13/viper" ) -func ResolveSSLFilesPath(gameId string, auto bool) (resolvedCertFile string, resolvedKeyFile string, err error) { - if auto { +func ResolveSSLFilesPath(gameId string, ssl internal.SSL) (resolvedCertFile string, resolvedKeyFile string, err error) { + if ssl.Auto { commonLogger.Println("Auto resolving SSL certificate and key files...") serverExe := executables.FindPath(executables.Filename(true, executables.Server)) if serverExe == "" { @@ -33,13 +33,13 @@ func ResolveSSLFilesPath(gameId string, auto bool) (resolvedCertFile string, res } var f os.FileInfo var path string - if f, path, err = common.ParsePath(viper.GetStringSlice("SSL.CertFile"), nil); err != nil || f.IsDir() { + if f, path, err = common.ParsePath(common.EnhancedViperStringToStringSlice(ssl.CertFile), nil); err != nil || f.IsDir() { err = fmt.Errorf("invalid certificate file") return } else { resolvedCertFile = path } - if f, path, err = common.ParsePath(viper.GetStringSlice("SSL.KeyFile"), nil); err != nil || f.IsDir() { + if f, path, err = common.ParsePath(common.EnhancedViperStringToStringSlice(ssl.KeyFile), nil); err != nil || f.IsDir() { err = fmt.Errorf("invalid key file") return } else { diff --git a/battle-server-manager/internal/config.go b/battle-server-manager/internal/config.go new file mode 100644 index 00000000..7e2ea96a --- /dev/null +++ b/battle-server-manager/internal/config.go @@ -0,0 +1,27 @@ +package internal + +type Executable struct { + Path string + ExtraArgs []string +} + +type Ports struct { + Bs int + WebSocket int + OutOfBand int +} + +type SSL struct { + Auto bool + CertFile string + KeyFile string +} + +type Configuration struct { + Region string + Name string + Host string + Executable Executable + Ports Ports + SSL SSL +} diff --git a/common/cert.go b/common/cert.go index 0ad34237..971d07e4 100644 --- a/common/cert.go +++ b/common/cert.go @@ -3,6 +3,8 @@ package common import ( "os" "path/filepath" + + "github.com/luskaner/ageLANServer/common/paths" ) func CertificatePairFolder(executablePath string) string { @@ -13,7 +15,7 @@ func CertificatePairFolder(executablePath string) string { if parentDir == "" { return "" } - folder := filepath.Join(parentDir, "resources", "certificates") + folder := filepath.Join(parentDir, paths.ResourcesDir, "certificates") if _, err := os.Stat(folder); os.IsNotExist(err) { if os.Mkdir(folder, 0755) != nil { return "" diff --git a/common/errors.go b/common/errors.go index 53b36345..3754eeaa 100644 --- a/common/errors.go +++ b/common/errors.go @@ -6,6 +6,7 @@ const ( ErrSignal ErrPidLock ErrFileLog + ErrConfigParse // ErrLast is only used as a marker to where to start, not a real error ErrLast ) diff --git a/common/executables/executables.go b/common/executables/executables.go index 88a893fc..c36147de 100644 --- a/common/executables/executables.go +++ b/common/executables/executables.go @@ -14,11 +14,16 @@ const ServerGenCert = "genCert" // Launcher +const Launcher = "launcher" const LauncherAgent = "agent" const LauncherConfig = "config" const LauncherConfigAdmin = "config-admin" const LauncherConfigAdminAgent = "config-admin-agent" +// Battle Server Manager + +const BattleServerManager = "battle-server-manager" + var directories = []string{ fmt.Sprintf(`%c`, filepath.Separator), fmt.Sprintf(`%c..%c`, filepath.Separator, filepath.Separator), diff --git a/common/executor/exec/ShellExecuteExW_windows.go b/common/executor/exec/ShellExecuteExW_windows.go index b7b2e866..3a5022c8 100644 --- a/common/executor/exec/ShellExecuteExW_windows.go +++ b/common/executor/exec/ShellExecuteExW_windows.go @@ -57,10 +57,10 @@ func shellExecuteEx(verb string, start bool, executable string, executableWorkin ret, _, err = procShellExecuteEx.Call(uintptr(unsafe.Pointer(info))) if ret == 0 { return - } else { - err = nil } + err = nil + if !start { _, err = windows.WaitForSingleObject(info.hProcess, windows.INFINITE) if err != nil { diff --git a/common/fileLock/fileLock.go b/common/fileLock/fileLock.go new file mode 100644 index 00000000..ae36da23 --- /dev/null +++ b/common/fileLock/fileLock.go @@ -0,0 +1,20 @@ +package fileLock + +import ( + "os" +) + +type BaseLock struct { + File *os.File +} + +func (l *BaseLock) close() { + if l.File != nil { + _ = l.File.Close() + l.File = nil + } +} + +func NewBaseLock(f *os.File) *BaseLock { + return &BaseLock{File: f} +} diff --git a/common/fileLock/fileLock_fallback.go b/common/fileLock/fileLock_fallback.go new file mode 100644 index 00000000..59d5e4da --- /dev/null +++ b/common/fileLock/fileLock_fallback.go @@ -0,0 +1,20 @@ +//go:build !darwin && !dragonfly && !freebsd && !illumos && !linux && !netbsd && !openbsd && !solaris && !windows + +package fileLock + +import ( + "os" +) + +// Lock The fallback does nothing. +type Lock struct{} + +func (l *Lock) Lock(_ *os.File) error { + return nil +} + +func (l *Lock) Unlock() error { + return nil +} + +func (l *Lock) clean() {} diff --git a/common/pidLock/pidLock_unix.go b/common/fileLock/fileLock_unix.go similarity index 50% rename from common/pidLock/pidLock_unix.go rename to common/fileLock/fileLock_unix.go index dd2b3af7..46776b2b 100644 --- a/common/pidLock/pidLock_unix.go +++ b/common/fileLock/fileLock_unix.go @@ -1,33 +1,24 @@ //go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd || solaris -package pidLock +package fileLock -import "syscall" +import ( + "os" + "syscall" +) type Lock struct { - Data + *BaseLock fd int } -func (l *Lock) Lock() error { +func (l *Lock) Lock(f *os.File) error { var err error - err, l.file = openFile() - if err != nil { - return err - } - l.fd = int(l.file.Fd()) + l.BaseLock = NewBaseLock(f) + l.fd = int(l.File.Fd()) err = syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB) if err != nil { - _ = l.file.Close() - l.file = nil - l.fd = 0 - return err - } - err = writePid(l.file) - if err != nil { - _ = l.file.Close() - l.file = nil - l.fd = 0 + l.clean() return err } return nil @@ -38,11 +29,11 @@ func (l *Lock) Unlock() error { if err != nil { return err } - err = removeFile(l.file) - if err != nil { - return err - } - l.fd = 0 - l.file = nil + l.clean() return nil } + +func (l *Lock) clean() { + l.close() + l.fd = 0 +} diff --git a/common/pidLock/pidLock_windows.go b/common/fileLock/fileLock_windows.go similarity index 55% rename from common/pidLock/pidLock_windows.go rename to common/fileLock/fileLock_windows.go index d9493cce..0d628262 100644 --- a/common/pidLock/pidLock_windows.go +++ b/common/fileLock/fileLock_windows.go @@ -1,22 +1,21 @@ -package pidLock +package fileLock import ( + "os" + "golang.org/x/sys/windows" ) type Lock struct { - Data + *BaseLock lock *windows.Overlapped handle windows.Handle } -func (l *Lock) Lock() error { +func (l *Lock) Lock(f *os.File) error { var err error - err, l.file = openFile() - if err != nil { - return err - } - l.handle = windows.Handle(l.file.Fd()) + l.BaseLock = NewBaseLock(f) + l.handle = windows.Handle(l.BaseLock.File.Fd()) l.lock = &windows.Overlapped{} err = windows.LockFileEx( l.handle, @@ -27,18 +26,8 @@ func (l *Lock) Lock() error { l.lock, ) if err != nil { - _ = l.file.Close() - l.file = nil - l.lock = nil - l.handle = 0 - return err - } - err = writePid(l.file) - if err != nil { - _ = l.file.Close() - l.file = nil - l.lock = nil - l.handle = 0 + _ = l.File.Close() + l.clean() return err } return nil @@ -49,12 +38,12 @@ func (l *Lock) Unlock() error { if err != nil { return err } - err = removeFile(l.file) - if err != nil { - return err - } + l.clean() + return nil +} + +func (l *Lock) clean() { + l.close() l.handle = 0 l.lock = nil - l.file = nil - return nil } diff --git a/common/fileLock/pidLock.go b/common/fileLock/pidLock.go new file mode 100644 index 00000000..1a4360d1 --- /dev/null +++ b/common/fileLock/pidLock.go @@ -0,0 +1,92 @@ +package fileLock + +import ( + "encoding/binary" + "os" + + "github.com/luskaner/ageLANServer/common/process" +) + +func openFile() (err error, f *os.File) { + var exe string + exe, err = os.Executable() + if err != nil { + return + } + var pidPath string + var proc *os.Process + pidPath, proc, err = process.Process(exe) + if err == nil && proc != nil { + return + } + f, err = os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY, 0644) + return +} + +func writePid(f *os.File) error { + pid := os.Getpid() + // If GetProcessStartTime fails, use 0 which disables start time validation + // but still allows the lock to function based on PID alone + startTime, _ := process.GetProcessStartTime(pid) + + data := make([]byte, process.PidFileSize) + binary.LittleEndian.PutUint64(data[0:8], uint64(pid)) + binary.LittleEndian.PutUint64(data[8:16], uint64(startTime)) + + err := f.Truncate(int64(len(data))) + if err != nil { + return err + } + _, err = f.Write(data) + if err != nil { + return err + } + return f.Sync() +} + +func removeFile(f *os.File) error { + err := f.Close() + if err != nil { + return err + } + err = os.Remove(f.Name()) + if err != nil { + return err + } + return nil +} + +type PidLock struct { + fileLock Lock +} + +func (l *PidLock) Lock() error { + //goland:noinspection ALL + err, file := openFile() + if err != nil { + return err + } + err = l.fileLock.Lock(file) + if err != nil { + return err + } + err = writePid(file) + if err != nil { + l.fileLock.clean() + return err + } + return nil +} + +func (l *PidLock) Unlock() error { + err := l.fileLock.Unlock() + if err != nil { + return err + } + err = removeFile(l.fileLock.BaseLock.File) + if err != nil { + return err + } + l.fileLock.clean() + return nil +} diff --git a/common/game/appx/appx_windows.go b/common/game/appx/appx_windows.go index 0380762f..d6b2f263 100644 --- a/common/game/appx/appx_windows.go +++ b/common/game/appx/appx_windows.go @@ -7,6 +7,7 @@ import ( "github.com/luskaner/ageLANServer/common" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" ) const ( @@ -15,12 +16,14 @@ const ( ) var ( - modkernel32 = windows.NewLazyDLL("kernel32.dll") - modkernelbase = windows.NewLazyDLL("kernelbase.dll") - procFindPackagesByPackageFamily = modkernelbase.NewProc("FindPackagesByPackageFamily") - procGetStagedPackagePathByFullName = modkernel32.NewProc("GetStagedPackagePathByFullName") + modkernelbase = windows.NewLazyDLL("kernelbase.dll") + procFindPackagesByPackageFamily = modkernelbase.NewProc("FindPackagesByPackageFamily") ) +func appxAPIAvailable() bool { + return procFindPackagesByPackageFamily.Find() == nil +} + const ( PackageFilterHead uint32 = 0x10 ErrorSuccess uint32 = 0 @@ -50,6 +53,9 @@ func FamilyName(gameTitle string) string { } func PackageFamilyNameToFullName(packageFamilyName string) (ok bool, fullName string) { + if !appxAPIAvailable() { + return + } pfnUTF16, err := windows.UTF16PtrFromString(packageFamilyName) if err != nil { return @@ -95,29 +101,22 @@ func PackageFamilyNameToFullName(packageFamilyName string) (ok bool, fullName st } func InstallLocation(packageFullName string) (ok bool, installLocation string) { - pfnUTF16, err := windows.UTF16PtrFromString(packageFullName) + key, err := registry.OpenKey( + registry.CURRENT_USER, + `SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\`+packageFullName, + registry.QUERY_VALUE, + ) if err != nil { return } - var bufferLength uint32 - result, _, _ := procGetStagedPackagePathByFullName.Call( - uintptr(unsafe.Pointer(pfnUTF16)), - uintptr(unsafe.Pointer(&bufferLength)), - uintptr(0), - ) - - if uint32(result) == ErrorInsufficientBuffer { - buffer := make([]uint16, bufferLength) - result, _, _ = procGetStagedPackagePathByFullName.Call( - uintptr(unsafe.Pointer(pfnUTF16)), - uintptr(unsafe.Pointer(&bufferLength)), - uintptr(unsafe.Pointer(&buffer[0])), - ) - if uint32(result) == ErrorSuccess { - installLocation = windows.UTF16ToString(buffer[:bufferLength-1]) - ok = true - } + defer func(key registry.Key) { + _ = key.Close() + }(key) + installLocation, _, err = key.GetStringValue("PackageRootFolder") + if err != nil { + return } + ok = true return } diff --git a/common/go.mod b/common/go.mod index 7655b54a..062f3d7b 100644 --- a/common/go.mod +++ b/common/go.mod @@ -1,6 +1,6 @@ module github.com/luskaner/ageLANServer/common -go 1.24.0 +go 1.25.5 require ( github.com/andygrunwald/vdf v1.1.0 @@ -8,19 +8,13 @@ require ( github.com/hairyhenderson/go-which v0.2.2 github.com/pelletier/go-toml/v2 v2.2.4 github.com/spf13/pflag v1.0.10 - github.com/spf13/viper v1.21.0 golang.org/x/sys v0.39.0 golang.org/x/term v0.38.0 mvdan.cc/sh/v3 v3.12.0 ) require ( - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/text v0.31.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.11.1 // indirect ) diff --git a/common/go.sum b/common/go.sum index cc1f7dd5..7ae21d9d 100644 --- a/common/go.sum +++ b/common/go.sum @@ -1,62 +1,17 @@ github.com/andygrunwald/vdf v1.1.0 h1:gmstp0R7DOepIZvWoSJY97ix7QOrsxpGPU6KusKXqvw= -github.com/andygrunwald/vdf v1.1.0/go.mod h1:f31AAs7HOKvs5B167iwLHwKuqKc4bE46Vdt7xQogA0o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/hairyhenderson/go-which v0.2.2 h1:yMyAHo4InxHiTAboIeOji8nZ5EXwIp116a2uo/MFkFI= -github.com/hairyhenderson/go-which v0.2.2/go.mod h1:vBfncX6hXWQhY1Qte8qQNWuJNnsGPqFLjgmwEETyOAo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= diff --git a/common/parse.go b/common/parse.go index a3bfa4f4..5631ed8f 100644 --- a/common/parse.go +++ b/common/parse.go @@ -8,17 +8,12 @@ import ( "runtime" "strings" - "github.com/spf13/viper" "mvdan.cc/sh/v3/shell" ) var reWinToLinVar *regexp.Regexp -func ParseCommandArgs(name string, values map[string]string, separateFields bool) (args []string, err error) { - return parseCommandArgs(viper.GetStringSlice(name), values, separateFields) -} - -func parseCommandArgs(value []string, values map[string]string, separateFields bool) (args []string, err error) { +func ParseCommandArgsFromSlice(value []string, values map[string]string, separateFields bool) (args []string, err error) { cmdArgs := strings.Join(value, " ") for key, val := range values { cmdArgs = strings.ReplaceAll(cmdArgs, fmt.Sprintf(`{%s}`, key), val) @@ -39,7 +34,7 @@ func parseCommandArgs(value []string, values map[string]string, separateFields b func ParsePath(value []string, values map[string]string) (file os.FileInfo, path string, err error) { var args []string - args, err = parseCommandArgs(value, values, false) + args, err = ParseCommandArgsFromSlice(value, values, false) if err != nil { return } @@ -54,3 +49,7 @@ func ParsePath(value []string, values map[string]string) (file os.FileInfo, path file, err = os.Stat(path) return } + +func EnhancedViperStringToStringSlice(value string) []string { + return []string{value} +} diff --git a/common/paths/paths.go b/common/paths/paths.go new file mode 100644 index 00000000..fd65ab92 --- /dev/null +++ b/common/paths/paths.go @@ -0,0 +1,8 @@ +package paths + +import "path/filepath" + +const ResourcesDir = "resources" +const ConfigDir = "config" + +var ConfigsPath = filepath.Join(ResourcesDir, ConfigDir) diff --git a/common/pidLock/pidLock.go b/common/pidLock/pidLock.go deleted file mode 100644 index 2dd07b38..00000000 --- a/common/pidLock/pidLock.go +++ /dev/null @@ -1,53 +0,0 @@ -package pidLock - -import ( - "os" - "strconv" - - "github.com/luskaner/ageLANServer/common/process" -) - -type Data struct { - file *os.File -} - -func openFile() (err error, f *os.File) { - var exe string - exe, err = os.Executable() - if err != nil { - return - } - var pidPath string - var proc *os.Process - pidPath, proc, err = process.Process(exe) - if err == nil && proc != nil { - return - } - f, err = os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY, 0644) - return -} - -func writePid(f *os.File) error { - str := strconv.Itoa(os.Getpid()) - err := f.Truncate(int64(len(str))) - if err != nil { - return err - } - _, err = f.WriteString(str) - if err != nil { - return err - } - return f.Sync() -} - -func removeFile(f *os.File) error { - err := f.Close() - if err != nil { - return err - } - err = os.Remove(f.Name()) - if err != nil { - return err - } - return nil -} diff --git a/common/pidLock/pidLock_fallback.go b/common/pidLock/pidLock_fallback.go deleted file mode 100644 index cfa151a3..00000000 --- a/common/pidLock/pidLock_fallback.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build !darwin && !dragonfly && !freebsd && !illumos && !linux && !netbsd && !openbsd && !solaris && !windows - -package pidLock - -type Lock struct { - Data -} - -func (l *Lock) Lock() error { - var err error - err, l.file = openFile() - if err != nil { - return err - } - err = writePid(l.file) - if err != nil { - return err - } - return nil -} - -func (l *Lock) Unlock() error { - err := removeFile(l.file) - if err != nil { - return err - } - l.file = nil - return nil -} diff --git a/common/process/process.go b/common/process/process.go index fa40ee57..6c4d6786 100644 --- a/common/process/process.go +++ b/common/process/process.go @@ -1,16 +1,18 @@ package process import ( + "encoding/binary" "errors" "os" "path/filepath" - "strconv" "time" mapset "github.com/deckarep/golang-set/v2" "github.com/luskaner/ageLANServer/common" ) +const PidFileSize = 16 // uint64 PID + uint64 StartTime + var waitDuration = 3 * time.Second func steamProcess(gameId string) string { @@ -61,7 +63,6 @@ func getPidPaths(exePath string) (paths []string) { func Process(exe string) (pidPath string, proc *os.Process, err error) { pidPaths := getPidPaths(exe) - var pid int for _, pidPath = range pidPaths { var data []byte var localErr error @@ -69,11 +70,21 @@ func Process(exe string) (pidPath string, proc *os.Process, err error) { if localErr != nil { continue } - pid, localErr = strconv.Atoi(string(data)) - if localErr != nil { + if len(data) != PidFileSize { + // Invalid format (old or corrupted), remove orphan file + // Error ignored: file may have been removed by concurrent process (race condition) + _ = os.Remove(pidPath) + continue + } + pid := int(binary.LittleEndian.Uint64(data[0:8])) + startTime := int64(binary.LittleEndian.Uint64(data[8:16])) + proc, err = FindProcessWithStartTime(pid, startTime) + if proc == nil { + // Process doesn't exist or startTime doesn't match, remove orphan file + // Error ignored: file may have been removed by concurrent process (race condition) + _ = os.Remove(pidPath) continue } - proc, err = FindProcess(pid) return } pidPath = pidPaths[0] diff --git a/common/process/process_darwin.go b/common/process/process_darwin.go index e0eefbe9..b1ada640 100644 --- a/common/process/process_darwin.go +++ b/common/process/process_darwin.go @@ -1,10 +1,83 @@ package process import ( - "os" + "errors" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" "time" ) -func WaitForProcess(_ *os.Process, _ *time.Duration) bool { - return true +func GetProcessStartTime(pid int) (int64, error) { + // Use ps command to get process start time (lstart format) + // This avoids brittle hardcoded struct offsets that vary between macOS versions/architectures + output, err := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "lstart=").Output() + if err != nil { + return 0, err + } + timeStr := strings.TrimSpace(string(output)) + if timeStr == "" { + return 0, errors.New("empty process start time") + } + t, err := parseProcessStartTime(timeStr) + if err != nil { + return 0, err + } + return t.UnixNano(), nil +} + +func parseProcessStartTime(timeStr string) (t time.Time, err error) { + // lstart format examples: + // "Tue Dec 3 10:30:00 2024" (single-digit day, space-padded) + // "Tue Dec 24 10:30:00 2024" (double-digit day) + // + // Note: lstart output is in local time without timezone info. + // We parse in local timezone and convert to Unix nanoseconds (UTC). + // Limitation: During DST transitions, local times can be ambiguous + // (e.g., 2:30 AM may occur twice when clocks fall back). This could + // cause rare false matches during the ~1 hour DST transition window. + t, err = time.ParseInLocation("Mon Jan _2 15:04:05 2006", timeStr, time.Local) + return +} + +// ProcessesPID returns a map of process names to their PIDs. +// Note: If multiple processes share the same name, only one PID is stored per name. +func ProcessesPID(names []string) map[string]uint32 { + processesPid := make(map[string]uint32) + + output, err := exec.Command("ps", "-axo", "pid,comm").Output() + if err != nil { + return processesPid + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines[1:] { // Skip header + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Split only on first whitespace to handle process names with spaces + // Format: " PID COMM" where COMM may contain spaces + fields := strings.SplitN(line, " ", 2) + if len(fields) < 2 { + continue + } + pidStr := strings.TrimSpace(fields[0]) + comm := strings.TrimSpace(fields[1]) + if pidStr == "" || comm == "" { + continue + } + pid, err := strconv.ParseUint(pidStr, 10, 32) + if err != nil { + continue + } + cmdlineName := filepath.Base(comm) + if slices.Contains(names, cmdlineName) { + processesPid[cmdlineName] = uint32(pid) + } + } + + return processesPid } diff --git a/common/process/process_linux.go b/common/process/process_linux.go new file mode 100644 index 00000000..41daa843 --- /dev/null +++ b/common/process/process_linux.go @@ -0,0 +1,85 @@ +package process + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "mvdan.cc/sh/v3/shell" +) + +// GetProcessStartTime returns the process start time as clock ticks since system boot. +// +// IMPORTANT: The returned value is NOT comparable across platforms: +// - Linux: clock ticks since boot (this implementation) +// - Windows: nanoseconds since epoch (absolute time) +// - Darwin: nanoseconds since epoch (absolute time) +// +// Values are only meaningful when compared within the same platform and boot session. +// This is sufficient for detecting PID reuse, as the primary use case is comparing +// a stored start time against the current start time of a process with the same PID. +func GetProcessStartTime(pid int) (int64, error) { + statPath := fmt.Sprintf("/proc/%d/stat", pid) + data, err := os.ReadFile(statPath) + if err != nil { + return 0, err + } + // Format: pid (comm) state ppid ... field 22 is starttime + // Find the last ')' to skip the comm field which may contain spaces/parentheses + statStr := string(data) + lastParen := strings.LastIndex(statStr, ")") + if lastParen == -1 { + return 0, errors.New("invalid stat format") + } + fields := strings.Fields(statStr[lastParen+1:]) + // After (comm), fields are: state(0), ppid(1), pgrp(2), session(3), tty_nr(4), tpgid(5), + // flags(6), minflt(7), cminflt(8), majflt(9), cmajflt(10), utime(11), stime(12), + // cutime(13), cstime(14), priority(15), nice(16), num_threads(17), itrealvalue(18), + // starttime(19) - in clock ticks since boot + if len(fields) < 20 { + return 0, errors.New("insufficient stat fields") + } + startTime, err := strconv.ParseInt(fields[19], 10, 64) + if err != nil { + return 0, err + } + return startTime, nil +} + +// ProcessesPID returns a map of process names to their PIDs. +// Note: If multiple processes share the same name, only one PID is stored per name. +func ProcessesPID(names []string) map[string]uint32 { + processesPid := make(map[string]uint32) + procs, err := os.ReadDir("/proc") + if err != nil { + return processesPid + } + + for _, proc := range procs { + var pid uint64 + if pid, err = strconv.ParseUint(proc.Name(), 10, 32); err == nil { + var cmdline []byte + cmdline, err = os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + if err != nil { + continue + } + cmdlineStr := strings.TrimSpace( + strings.ReplaceAll(strings.ReplaceAll(string(cmdline), "\x00", " "), "\\", "/"), + ) + var args []string + args, err = shell.Fields(cmdlineStr, nil) + if err != nil || len(args) == 0 { + continue + } + cmdlineName := filepath.Base(args[0]) + if slices.Contains(names, cmdlineName) { + processesPid[cmdlineName] = uint32(pid) + } + } + } + return processesPid +} diff --git a/common/process/process_notwin.go b/common/process/process_notwin.go new file mode 100644 index 00000000..d25f830f --- /dev/null +++ b/common/process/process_notwin.go @@ -0,0 +1,55 @@ +//go:build !windows + +// This file provides FindProcess and FindProcessWithStartTime for all non-Windows systems. +// It relies on GetProcessStartTime being defined in platform-specific files: +// - process_linux.go for Linux +// - process_darwin.go for Darwin +// - process_other.go for other Unix systems (fallback) + +package process + +import ( + "errors" + "os" + + "golang.org/x/sys/unix" +) + +func FindProcess(pid int) (proc *os.Process, err error) { + return FindProcessWithStartTime(pid, 0) +} + +func FindProcessWithStartTime(pid int, expectedStartTime int64) (proc *os.Process, err error) { + proc, err = os.FindProcess(pid) + if err != nil { + return + } + if err = proc.Signal(unix.Signal(0)); err != nil { + if errors.Is(err, unix.EPERM) { + // Process exists but we don't have permission to signal it + err = nil + } else { + proc = nil + return + } + } + if expectedStartTime != 0 { + actualStartTime, startErr := GetProcessStartTime(pid) + if startErr != nil { + if errors.Is(startErr, unix.EPERM) { + // Process exists but we can't validate start time due to permissions + // Treat as valid since Signal(0) already confirmed existence + return + } + proc = nil + err = startErr + return + } + if actualStartTime != expectedStartTime { + proc = nil + err = errors.New("process start time mismatch") + return + } + } + return +} diff --git a/common/process/process_other.go b/common/process/process_other.go index 2eec2e65..921d740d 100644 --- a/common/process/process_other.go +++ b/common/process/process_other.go @@ -1,25 +1,28 @@ -//go:build !windows +//go:build !windows && !darwin && !linux + +// This file provides fallback implementations for unsupported Unix systems. +// It implements GetProcessStartTime, WaitForProcess, and ProcessesPID with +// minimal functionality to allow the package to compile and run on any Unix system. package process import ( - "errors" "os" - - "golang.org/x/sys/unix" + "time" ) -func FindProcess(pid int) (proc *os.Process, err error) { - proc, err = os.FindProcess(pid) - if err != nil { - return - } - if err = proc.Signal(unix.Signal(0)); err != nil { - if errors.Is(err, unix.EPERM) { - err = nil - } else { - proc = nil - } - } - return +func GetProcessStartTime(_ int) (int64, error) { + // Fallback for unsupported Unix systems - always return 0 + // This disables startTime validation but maintains basic functionality + return 0, nil +} + +func WaitForProcess(_ *os.Process, _ *time.Duration) bool { + return true +} + +// ProcessesPID returns a map of process names to their PIDs. +// Note: If multiple processes share the same name, only one PID is stored per name. +func ProcessesPID(_ []string) map[string]uint32 { + return make(map[string]uint32) } diff --git a/common/process/process_unix.go b/common/process/process_unix.go index 86dbd0d4..57999c1a 100644 --- a/common/process/process_unix.go +++ b/common/process/process_unix.go @@ -1,17 +1,18 @@ -//go:build !windows && !darwin +//go:build linux || darwin + +// This file provides shared Unix implementations for Linux and Darwin. +// It works in conjunction with: +// - process_linux.go: provides GetProcessStartTime and ProcessesPID for Linux +// - process_darwin.go: provides GetProcessStartTime and ProcessesPID for Darwin +// - process_notwin.go: provides FindProcess and FindProcessWithStartTime for all non-Windows package process import ( - "fmt" "os" - "path/filepath" - "slices" - "strconv" - "strings" "time" - "mvdan.cc/sh/v3/shell" + "golang.org/x/sys/unix" ) func WaitForProcess(proc *os.Process, duration *time.Duration) bool { @@ -19,16 +20,13 @@ func WaitForProcess(proc *os.Process, duration *time.Duration) bool { if duration == nil { t *= 10 } - procPath := fmt.Sprintf("/proc/%d", proc.Pid) - processExists := func(path string) bool { - if _, err := os.Stat(procPath); os.IsNotExist(err) { - return true - } - return false + processGone := func() bool { + // Signal(0) returns nil if process exists, error if it doesn't + return proc.Signal(unix.Signal(0)) != nil } if duration == nil { for { - if processExists(procPath) { + if processGone() { return true } time.Sleep(t) @@ -40,7 +38,7 @@ func WaitForProcess(proc *os.Process, duration *time.Duration) bool { case <-timeout: return false default: - if processExists(procPath) { + if processGone() { return true } time.Sleep(t) @@ -48,35 +46,3 @@ func WaitForProcess(proc *os.Process, duration *time.Duration) bool { } } } - -func ProcessesPID(names []string) map[string]uint32 { - processesPid := make(map[string]uint32) - procs, err := os.ReadDir("/proc") - if err != nil { - return processesPid - } - - for _, proc := range procs { - var pid uint64 - if pid, err = strconv.ParseUint(proc.Name(), 10, 32); err == nil { - var cmdline []byte - cmdline, err = os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) - if err != nil { - continue - } - cmdlineStr := strings.TrimSpace( - strings.ReplaceAll(strings.ReplaceAll(string(cmdline), "\x00", " "), "\\", "/"), - ) - var args []string - args, err = shell.Fields(cmdlineStr, nil) - if err != nil || len(args) == 0 { - continue - } - cmdlineName := filepath.Base(args[0]) - if slices.Contains(names, cmdlineName) { - processesPid[cmdlineName] = uint32(pid) - } - } - } - return processesPid -} diff --git a/common/process/process_windows.go b/common/process/process_windows.go index 6bfba2b8..13ebfc2c 100644 --- a/common/process/process_windows.go +++ b/common/process/process_windows.go @@ -31,6 +31,8 @@ func WaitForProcess(proc *os.Process, duration *time.Duration) bool { return err == nil && event == uint32(windows.WAIT_OBJECT_0) } +// ProcessesPID returns a map of process names to their PIDs. +// Note: If multiple processes share the same name, only one PID is stored per name. func ProcessesPID(names []string) map[string]uint32 { name := func(entry *windows.ProcessEntry32) string { return windows.UTF16ToString(entry.ExeFile[:]) @@ -80,7 +82,28 @@ func processesEntry(matches func(entry *windows.ProcessEntry32) bool, firstOnly return entries } +func GetProcessStartTime(pid int) (int64, error) { + handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return 0, err + } + defer func(handle windows.Handle) { + _ = windows.CloseHandle(handle) + }(handle) + + var creationTime, exitTime, kernelTime, userTime windows.Filetime + err = windows.GetProcessTimes(handle, &creationTime, &exitTime, &kernelTime, &userTime) + if err != nil { + return 0, err + } + return creationTime.Nanoseconds(), nil +} + func FindProcess(pid int) (proc *os.Process, err error) { + return FindProcessWithStartTime(pid, 0) +} + +func FindProcessWithStartTime(pid int, expectedStartTime int64) (proc *os.Process, err error) { proc, err = os.FindProcess(pid) if errors.Is(err, windows.ERROR_INVALID_PARAMETER) { err = nil @@ -90,9 +113,24 @@ func FindProcess(pid int) (proc *os.Process, err error) { }, true) if len(entries) == 0 { proc = nil - } else if err != nil { + return + } + if err != nil { proc = &os.Process{Pid: pid} err = nil } + if expectedStartTime != 0 { + actualStartTime, startErr := GetProcessStartTime(pid) + if startErr != nil { + proc = nil + err = startErr + return + } + if actualStartTime != expectedStartTime { + proc = nil + err = errors.New("process start time mismatch") + return + } + } return } diff --git a/common/resolve.go b/common/resolve.go index 25ce271f..0be90675 100644 --- a/common/resolve.go +++ b/common/resolve.go @@ -18,7 +18,7 @@ func init() { ClearCache() } -func HostToIps(host string) []net.IP { +func domainToIps(host string) []net.IP { ips, err := net.LookupIP(host) if err != nil { return nil @@ -102,22 +102,22 @@ func HostOrIpToIps(host string) []string { } } return ips - } else { - cached, cachedIps := cachedHostToIps(host) - if cached { - return cachedIps.Clone().ToSlice() - } - var ips []string - ipsFromDns := HostToIps(host) - if ipsFromDns != nil { - for _, ipRaw := range ipsFromDns { - ipStr := ipRaw.String() - ips = append(ips, ipStr) - CacheMapping(host, ipStr) - } + } + + cached, cachedIps := cachedHostToIps(host) + if cached { + return cachedIps.Clone().ToSlice() + } + var ips []string + ipsFromDns := domainToIps(host) + if ipsFromDns != nil { + for _, ipRaw := range ipsFromDns { + ipStr := ipRaw.String() + ips = append(ips, ipStr) + CacheMapping(host, ipStr) } - return ips } + return ips } func HostOrIpToIpsSet(host string) mapset.Set[string] { diff --git a/go.work.example b/go.work.example index edd52a97..ded338bf 100644 --- a/go.work.example +++ b/go.work.example @@ -1,4 +1,4 @@ -go 1.24.0 +go 1.25.5 // Necessary for launcher-config's internal.readCertsFromFile godebug x509negativeserial=1 @@ -15,7 +15,8 @@ use ( launcher-config-admin-agent server server-genCert - server-replay + tools/server-replay + tools/scripts ) replace github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0 => ./battle-server-broadcast diff --git a/launcher-agent/go.mod b/launcher-agent/go.mod index 467aadd7..54c576e5 100644 --- a/launcher-agent/go.mod +++ b/launcher-agent/go.mod @@ -1,5 +1,5 @@ module github.com/luskaner/ageLANServer/launcher-agent -go 1.24.0 +go 1.25.5 require github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0 diff --git a/launcher-agent/go.sum b/launcher-agent/go.sum index 78ce296a..c7eca025 100644 --- a/launcher-agent/go.sum +++ b/launcher-agent/go.sum @@ -1,2 +1 @@ github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0 h1:EB9EKyv7koSQSIR3+DvnwOjpGzCUD2HjtqPuFOFOj/k= -github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0/go.mod h1:USeNRD3Mdc+K6EMtZaexx+ngXZz7kTCCMbuz8dt/vDo= diff --git a/launcher-agent/main.go b/launcher-agent/main.go index 6e12a16b..c6d26c63 100644 --- a/launcher-agent/main.go +++ b/launcher-agent/main.go @@ -10,8 +10,8 @@ import ( "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/executor/exec" + "github.com/luskaner/ageLANServer/common/fileLock" "github.com/luskaner/ageLANServer/common/logger" - "github.com/luskaner/ageLANServer/common/pidLock" commonProcess "github.com/luskaner/ageLANServer/common/process" "github.com/luskaner/ageLANServer/launcher-agent/internal" "github.com/luskaner/ageLANServer/launcher-agent/internal/watch" @@ -20,7 +20,7 @@ import ( func main() { commonLogger.Initialize(os.Stdout) - lock := &pidLock.Lock{} + lock := &fileLock.PidLock{} exitCode := common.ErrSuccess if err := lock.Lock(); err != nil { commonLogger.Println("Failed to lock pid file. Kill process 'agent' if it is running in your task manager.") diff --git a/launcher-common/go.mod b/launcher-common/go.mod index f3c64bb8..3dfef935 100644 --- a/launcher-common/go.mod +++ b/launcher-common/go.mod @@ -1,29 +1,18 @@ module github.com/luskaner/ageLANServer/launcher-common -go 1.24.0 +go 1.25.5 require ( github.com/deckarep/golang-set/v2 v2.8.0 github.com/hairyhenderson/go-which v0.2.2 - github.com/luskaner/ageLANServer/common v0.0.0-20251209201001-a9a449149c80 github.com/spf13/cobra v1.10.2 golang.org/x/sys v0.39.0 ) require ( - github.com/andygrunwald/vdf v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - mvdan.cc/sh/v3 v3.12.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect ) diff --git a/launcher-common/go.sum b/launcher-common/go.sum index 2fdda593..d4184caa 100644 --- a/launcher-common/go.sum +++ b/launcher-common/go.sum @@ -1,71 +1,10 @@ -github.com/andygrunwald/vdf v1.1.0 h1:gmstp0R7DOepIZvWoSJY97ix7QOrsxpGPU6KusKXqvw= -github.com/andygrunwald/vdf v1.1.0/go.mod h1:f31AAs7HOKvs5B167iwLHwKuqKc4bE46Vdt7xQogA0o= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/hairyhenderson/go-which v0.2.2 h1:yMyAHo4InxHiTAboIeOji8nZ5EXwIp116a2uo/MFkFI= -github.com/hairyhenderson/go-which v0.2.2/go.mod h1:vBfncX6hXWQhY1Qte8qQNWuJNnsGPqFLjgmwEETyOAo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/luskaner/ageLANServer/common v0.0.0-20251209201001-a9a449149c80 h1:YorECi2Y8CRMoq3nJ757snhgcwM/wVQaZIvLSa9n/JY= -github.com/luskaner/ageLANServer/common v0.0.0-20251209201001-a9a449149c80/go.mod h1:9iT1rLpLyHGdPfYxbEgyY67Z1tMvFJ59vsCTjd8jxGw= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= diff --git a/launcher-common/hosts/hosts.go b/launcher-common/hosts/hosts.go index 163759d8..d6352e0c 100644 --- a/launcher-common/hosts/hosts.go +++ b/launcher-common/hosts/hosts.go @@ -271,6 +271,7 @@ func AddHosts(gameId string, hostFilePath string, lineEnding string, flushFn fun } var hostsFile *os.File mappings := Mappings(gameId) + //goland:noinspection ALL err, hostsFile = missingIpMappings(&mappings, hostFilePath) if err != nil { return diff --git a/launcher-common/serverKill/serverKill_windows.go b/launcher-common/serverKill/serverKill_windows.go index a9866bda..040f0e56 100644 --- a/launcher-common/serverKill/serverKill_windows.go +++ b/launcher-common/serverKill/serverKill_windows.go @@ -25,6 +25,7 @@ func Do(path string) error { return fmt.Errorf("could not resolve local IPs") } for _, ip := range ips { + //goland:noinspection ALL resp, err := client.Post(fmt.Sprintf("https://%s/shutdown", ip), "", nil) if err == nil && resp.StatusCode == http.StatusOK { break diff --git a/launcher-config-admin-agent/go.mod b/launcher-config-admin-agent/go.mod index 2a6be635..8e96049b 100644 --- a/launcher-config-admin-agent/go.mod +++ b/launcher-config-admin-agent/go.mod @@ -1,7 +1,7 @@ module github.com/luskaner/ageLANServer/launcher-config-admin-agent -go 1.24.0 +go 1.25.5 require github.com/Microsoft/go-winio v0.6.2 -require golang.org/x/sys v0.38.0 // indirect +require golang.org/x/sys v0.39.0 // indirect diff --git a/launcher-config-admin-agent/go.sum b/launcher-config-admin-agent/go.sum index 1002db24..c65371bb 100644 --- a/launcher-config-admin-agent/go.sum +++ b/launcher-config-admin-agent/go.sum @@ -1,4 +1,2 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= diff --git a/launcher-config-admin-agent/internal/ipc/ipc.go b/launcher-config-admin-agent/internal/ipc/ipc.go index 6b28efb8..e6b9decb 100644 --- a/launcher-config-admin-agent/internal/ipc/ipc.go +++ b/launcher-config-admin-agent/internal/ipc/ipc.go @@ -110,9 +110,9 @@ func handleSetUp(logRoot string, decoder *gob.Decoder) int { str += "invalid" } return internal.ErrCertInvalid - } else { - str += "OK" } + + str += "OK" commonLogger.Println(str) } else { commonLogger.Println("No certificate") diff --git a/launcher-config-admin-agent/main.go b/launcher-config-admin-agent/main.go index 06658e34..6c64b4d4 100644 --- a/launcher-config-admin-agent/main.go +++ b/launcher-config-admin-agent/main.go @@ -8,8 +8,8 @@ import ( "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/executor" + "github.com/luskaner/ageLANServer/common/fileLock" "github.com/luskaner/ageLANServer/common/logger" - "github.com/luskaner/ageLANServer/common/pidLock" launcherCommon "github.com/luskaner/ageLANServer/launcher-common" "github.com/luskaner/ageLANServer/launcher-config-admin-agent/internal" "github.com/luskaner/ageLANServer/launcher-config-admin-agent/internal/ipc" @@ -23,7 +23,7 @@ func main() { } else { logRoot = "" } - lock := &pidLock.Lock{} + lock := &fileLock.PidLock{} if err := lock.Lock(); err != nil { commonLogger.Println("Failed to lock pid file. Kill process 'config-admin-agent' if it is running in your task manager.") commonLogger.CloseFileLog() diff --git a/launcher-config-admin/go.mod b/launcher-config-admin/go.mod index e4429712..0436ca3c 100644 --- a/launcher-config-admin/go.mod +++ b/launcher-config-admin/go.mod @@ -1,6 +1,6 @@ module github.com/luskaner/ageLANServer/launcher-config-admin -go 1.24.0 +go 1.25.5 require ( github.com/deckarep/golang-set/v2 v2.8.0 diff --git a/launcher-config-admin/go.sum b/launcher-config-admin/go.sum index 7461b965..1cf17f07 100644 --- a/launcher-config-admin/go.sum +++ b/launcher-config-admin/go.sum @@ -1,13 +1,4 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/launcher-config-admin/internal/cmd/revert.go b/launcher-config-admin/internal/cmd/revert.go index 6dcbd050..1f2f0f33 100644 --- a/launcher-config-admin/internal/cmd/revert.go +++ b/launcher-config-admin/internal/cmd/revert.go @@ -21,10 +21,10 @@ func trustCertificates(certificates []*x509.Certificate) bool { if err := cert.TrustCertificates(false, certificates); err == nil { commonLogger.Println("Successfully added local certificate") return true - } else { - commonLogger.Println("Failed to add local certificate") - return false } + + commonLogger.Println("Failed to add local certificate") + return false } var revertCmd = &cobra.Command{ diff --git a/launcher-config-admin/internal/cmd/setUp.go b/launcher-config-admin/internal/cmd/setUp.go index 6f496bd4..37c49438 100644 --- a/launcher-config-admin/internal/cmd/setUp.go +++ b/launcher-config-admin/internal/cmd/setUp.go @@ -22,10 +22,10 @@ func untrustCertificate() bool { if _, err := cert.UntrustCertificates(false); err == nil { commonLogger.Println("Successfully removed local certificate") return true - } else { - commonLogger.Println("Failed to remove local certificate") - return false } + + commonLogger.Println("Failed to remove local certificate") + return false } var setUpCmd = &cobra.Command{ diff --git a/launcher-config/go.mod b/launcher-config/go.mod index fffeacec..a09d0756 100644 --- a/launcher-config/go.mod +++ b/launcher-config/go.mod @@ -1,6 +1,6 @@ module github.com/luskaner/ageLANServer/launcher-config -go 1.24.0 +go 1.25.5 require ( github.com/Microsoft/go-winio v0.6.2 @@ -11,5 +11,5 @@ require ( require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.39.0 // indirect ) diff --git a/launcher-config/go.sum b/launcher-config/go.sum index 9e35bfbb..98ce34dc 100644 --- a/launcher-config/go.sum +++ b/launcher-config/go.sum @@ -1,17 +1,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= diff --git a/launcher-config/internal/admin/admin.go b/launcher-config/internal/admin/admin.go index 0a980618..a602c8a0 100644 --- a/launcher-config/internal/admin/admin.go +++ b/launcher-config/internal/admin/admin.go @@ -28,43 +28,43 @@ func RunSetUp(logRoot string, ipToMap net.IP, addCertData []byte) (err error, ex exitCode = common.ErrGeneral if ipc != nil { return runSetUpAgent(ipToMap, addCertData) - } else { - var certificate *x509.Certificate - if addCertData != nil { - certificate = cert.BytesToCertificate(addCertData) - if certificate == nil { - exitCode = internal.ErrUserCertAddParse - return - } - } - var result *exec.Result - var file *commonLogger.Root - if logRoot != "" { - if err, file = commonLogger.NewFile(logRoot, "", true); err != nil { - exitCode = common.ErrFileLog - return - } - } - var suffix string - if len(addCertData) > 0 { - suffix = "_cert" - } else { - suffix = "_hosts" + } + + var certificate *x509.Certificate + if addCertData != nil { + certificate = cert.BytesToCertificate(addCertData) + if certificate == nil { + exitCode = internal.ErrUserCertAddParse + return } - if bufferErr := file.Buffer("config-admin_setup"+suffix, func(writer io.Writer) { - result = executor.RunSetUp(cmd.GameId, ipToMap, certificate, file.Folder(), writer, func(options exec.Options) { - if writer != nil { - options.Stdout = writer - options.Stderr = writer - } - }) - }); bufferErr == nil { - err, exitCode = result.Err, result.ExitCode - } else { - err = bufferErr + } + var result *exec.Result + var file *commonLogger.Root + if logRoot != "" { + if err, file = commonLogger.NewFile(logRoot, "", true); err != nil { exitCode = common.ErrFileLog + return } } + var suffix string + if len(addCertData) > 0 { + suffix = "_cert" + } else { + suffix = "_hosts" + } + if bufferErr := file.Buffer("config-admin_setup"+suffix, func(writer io.Writer) { + result = executor.RunSetUp(cmd.GameId, ipToMap, certificate, file.Folder(), writer, func(options exec.Options) { + if writer != nil { + options.Stdout = writer + options.Stderr = writer + } + }) + }); bufferErr == nil { + err, exitCode = result.Err, result.ExitCode + } else { + err = bufferErr + exitCode = common.ErrFileLog + } return } @@ -179,9 +179,9 @@ func runRevertAgent(unmapIPs bool, removeCert bool) (err error, exitCode int) { if err = encoder.Encode(commonIpc.Revert); err != nil { commonLogger.Println(str + "Could not encode") return - } else { - commonLogger.Println(str + "OK") } + + commonLogger.Println(str + "OK") str = "<- Exit Code: " if err = decoder.Decode(&exitCode); err != nil || exitCode != common.ErrSuccess { if err != nil { diff --git a/launcher-config/internal/cmd/revert.go b/launcher-config/internal/cmd/revert.go index a9a10fae..461c19f8 100644 --- a/launcher-config/internal/cmd/revert.go +++ b/launcher-config/internal/cmd/revert.go @@ -27,10 +27,10 @@ func addUserCerts(removedUserCerts []*x509.Certificate) bool { if err := wrapper.AddUserCerts(removedUserCerts); err == nil { commonLogger.Println("Successfully added user certificate") return true - } else { - commonLogger.Println("Failed to add user certificate") - return false } + + commonLogger.Println("Failed to add user certificate") + return false } func backupMetadata() bool { @@ -38,10 +38,10 @@ func backupMetadata() bool { if userData.Metadata(cmd.GameId).Backup() { commonLogger.Println("Successfully backed up metadata") return true - } else { - commonLogger.Println("Failed to back up metadata") - return false } + + commonLogger.Println("Failed to back up metadata") + return false } func backupProfiles() bool { @@ -49,10 +49,10 @@ func backupProfiles() bool { if userData.BackupProfiles(cmd.GameId) { commonLogger.Println("Successfully backed up profiles") return true - } else { - commonLogger.Println("Failed to back up profiles") - return false } + + commonLogger.Println("Failed to back up profiles") + return false } func addCaCerts(removedCaCerts []*x509.Certificate) bool { @@ -60,10 +60,10 @@ func addCaCerts(removedCaCerts []*x509.Certificate) bool { if err := internal.NewCACert(cmd.GameId, gamePath).Append(removedCaCerts); err == nil { commonLogger.Println("Successfully restored game's certificate store.") return true - } else { - commonLogger.Println("Failed to restore game's certificate store.") - return false } + + commonLogger.Println("Failed to restore game's certificate store.") + return false } func undoRevert() { diff --git a/launcher-config/internal/cmd/setUp.go b/launcher-config/internal/cmd/setUp.go index 853b862d..e7313e04 100644 --- a/launcher-config/internal/cmd/setUp.go +++ b/launcher-config/internal/cmd/setUp.go @@ -30,10 +30,10 @@ func removeUserCert() bool { if _, err := wrapper.RemoveUserCerts(); err == nil { commonLogger.Println("Successfully removed user certificate") return true - } else { - commonLogger.Println("Failed to remove user certificate") - return false } + + commonLogger.Println("Failed to remove user certificate") + return false } func restoreMetadata() bool { @@ -41,10 +41,10 @@ func restoreMetadata() bool { if userData.Metadata(cmd.GameId).Restore() { commonLogger.Println("Successfully restored metadata") return true - } else { - commonLogger.Println("Failed to restore metadata") - return false } + + commonLogger.Println("Failed to restore metadata") + return false } func restoreProfiles() bool { @@ -52,10 +52,10 @@ func restoreProfiles() bool { if userData.RestoreProfiles(cmd.GameId, true) { commonLogger.Println("Successfully restored profiles") return true - } else { - commonLogger.Println("Failed to restore profiles") - return false } + + commonLogger.Println("Failed to restore profiles") + return false } func restoreGameCert() bool { @@ -63,10 +63,10 @@ func restoreGameCert() bool { if _, err := internal.NewCACert(cmd.GameId, gamePath).Restore(); err == nil { commonLogger.Println("Successfully restored game's certificate store.") return true - } else { - commonLogger.Println("Failed to restore game's certificate store.") - return false } + + commonLogger.Println("Failed to restore game's certificate store.") + return false } func undoSetUp() { diff --git a/launcher/README.md b/launcher/README.md index 531401f3..ea431e29 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -6,7 +6,7 @@ system and reverting that configuration upon exit. ## Minimum system Requirements - Windows without S edition/mode (recommended): - - 10 on x86-64 (recommended). + - 7 on x86-64 (10 or higher recommended). - 11 on ARM. - Linux with kernel 3.2: - x86-64 (recommended). diff --git a/launcher/go.mod b/launcher/go.mod index 981412a8..9e4878b8 100644 --- a/launcher/go.mod +++ b/launcher/go.mod @@ -1,33 +1,32 @@ module github.com/luskaner/ageLANServer/launcher -go 1.24.0 +go 1.25.5 require ( github.com/deckarep/golang-set/v2 v2.8.0 github.com/google/uuid v1.6.0 github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0 - github.com/luskaner/ageLANServer/common v0.0.0-20251209201001-a9a449149c80 - github.com/luskaner/ageLANServer/launcher-common v0.0.0-20251209201001-a9a449149c80 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 golang.org/x/net v0.48.0 golang.org/x/sys v0.39.0 - mvdan.cc/sh/v3 v3.12.0 ) require ( - github.com/andygrunwald/vdf v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/hairyhenderson/go-which v0.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/launcher/go.sum b/launcher/go.sum index 0e8c9028..02f863dd 100644 --- a/launcher/go.sum +++ b/launcher/go.sum @@ -1,79 +1,29 @@ -github.com/andygrunwald/vdf v1.1.0 h1:gmstp0R7DOepIZvWoSJY97ix7QOrsxpGPU6KusKXqvw= -github.com/andygrunwald/vdf v1.1.0/go.mod h1:f31AAs7HOKvs5B167iwLHwKuqKc4bE46Vdt7xQogA0o= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hairyhenderson/go-which v0.2.2 h1:yMyAHo4InxHiTAboIeOji8nZ5EXwIp116a2uo/MFkFI= -github.com/hairyhenderson/go-which v0.2.2/go.mod h1:vBfncX6hXWQhY1Qte8qQNWuJNnsGPqFLjgmwEETyOAo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0 h1:EB9EKyv7koSQSIR3+DvnwOjpGzCUD2HjtqPuFOFOj/k= github.com/luskaner/ageLANServer/battle-server-broadcast v1.3.0/go.mod h1:USeNRD3Mdc+K6EMtZaexx+ngXZz7kTCCMbuz8dt/vDo= -github.com/luskaner/ageLANServer/common v0.0.0-20251209201001-a9a449149c80 h1:YorECi2Y8CRMoq3nJ757snhgcwM/wVQaZIvLSa9n/JY= -github.com/luskaner/ageLANServer/common v0.0.0-20251209201001-a9a449149c80/go.mod h1:9iT1rLpLyHGdPfYxbEgyY67Z1tMvFJ59vsCTjd8jxGw= -github.com/luskaner/ageLANServer/launcher-common v0.0.0-20251209201001-a9a449149c80 h1:C4koaKIzEieguv13Bmf6H0qHv3Qd0ZcC5aAZCdlJBVE= -github.com/luskaner/ageLANServer/launcher-common v0.0.0-20251209201001-a9a449149c80/go.mod h1:cxKILXSHTy2XKn7LPT/FdE8fAm/HDNQAPx/w6F4dhsA= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= diff --git a/launcher/internal/cmd/root.go b/launcher/internal/cmd/root.go index 8d1ede28..d4b7ac67 100644 --- a/launcher/internal/cmd/root.go +++ b/launcher/internal/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "bufio" + "errors" "fmt" "io" "net" @@ -14,17 +15,19 @@ import ( "slices" "strconv" "strings" + "sync/atomic" "syscall" mapset "github.com/deckarep/golang-set/v2" "github.com/google/uuid" "github.com/luskaner/ageLANServer/common" - "github.com/luskaner/ageLANServer/common/cmd" + commonCmd "github.com/luskaner/ageLANServer/common/cmd" "github.com/luskaner/ageLANServer/common/executables" commonExecutor "github.com/luskaner/ageLANServer/common/executor" "github.com/luskaner/ageLANServer/common/executor/exec" + "github.com/luskaner/ageLANServer/common/fileLock" commonLogger "github.com/luskaner/ageLANServer/common/logger" - "github.com/luskaner/ageLANServer/common/pidLock" + "github.com/luskaner/ageLANServer/common/paths" commonProcess "github.com/luskaner/ageLANServer/common/process" launcherCommon "github.com/luskaner/ageLANServer/launcher-common" "github.com/luskaner/ageLANServer/launcher-common/cert" @@ -42,94 +45,109 @@ const autoValue = "auto" const trueValue = "true" const falseValue = "false" -var configPaths = []string{"resources", "."} +var configPaths = []string{paths.ResourcesDir, "."} var config = &cmdUtils.Config{} +var v = viper.New() var ( - Version string - cfgFile string - gameCfgFile string - gameId string - autoTrueFalseValues = mapset.NewThreadUnsafeSet[string](autoValue, trueValue, falseValue) - canTrustCertificateValues = mapset.NewThreadUnsafeSet[string](falseValue, "user", "local") - canBroadcastBattleServerValues = mapset.NewThreadUnsafeSet[string](autoValue, falseValue) - serverBattleServerManagerRunValues = mapset.NewThreadUnsafeSet[string](trueValue, falseValue, "required") - rootCmd = &cobra.Command{ + Version string + cfgFile string + gameCfgFile string + gameId string + autoTrueFalseValues = mapset.NewThreadUnsafeSet[string](autoValue, trueValue, falseValue) + canTrustCertificateValues = mapset.NewThreadUnsafeSet[string](falseValue, "user", "local") + canBroadcastBattleServerValues = mapset.NewThreadUnsafeSet[string](autoValue, falseValue) + requiredTrueFalseValues = mapset.NewThreadUnsafeSet[string](trueValue, falseValue, "required") + rootCmd = &cobra.Command{ Use: filepath.Base(os.Args[0]), Short: "launcher discovers and configures AoE: DE, AoE 2: DE and AoE 3: DE, and AoM: RT to connect to the local LAN 'server'", Long: "launcher discovers or starts a local LAN 'server', configures and executes the game launcher to connect to it", Run: func(_ *cobra.Command, _ []string) { - lock := &pidLock.Lock{} + lock := &fileLock.PidLock{} if err := lock.Lock(); err != nil { logger.Println("Failed to lock pid file. Kill process 'launcher' if it is running in your task manager.") logger.Println(err.Error()) os.Exit(common.ErrPidLock) } - initConfig() + cfg := initConfig() + logger.LogEnabled = cfg.Config.Log if err := logger.OpenMainFileLog(gameId); err != nil { logger.Println("Failed to open file log") logger.Println(err.Error()) os.Exit(common.ErrFileLog) } - var errorCode = common.ErrSuccess + var errorCode atomic.Int32 + errorCode.Store(int32(common.ErrSuccess)) defer func() { if r := recover(); r != nil { logger.Println(r) logger.Println(string(debug.Stack())) - errorCode = common.ErrGeneral + errorCode.Store(int32(common.ErrGeneral)) } - if errorCode != common.ErrSuccess { + if errorCode.Load() != int32(common.ErrSuccess) { config.Revert() } logger.WriteFileLog(gameId, "before exit") commonLogger.CloseFileLog() _ = lock.Unlock() - os.Exit(errorCode) + os.Exit(int(errorCode.Load())) }() logger.WriteFileLog(gameId, "start") isAdmin := commonExecutor.IsAdmin() - canTrustCertificate := viper.GetString("Config.CanTrustCertificate") + canTrustCertificate := cfg.Config.CanTrustCertificate if runtime.GOOS != "windows" { canTrustCertificateValues.Remove("user") } if !canTrustCertificateValues.Contains(canTrustCertificate) { logger.Printf("Invalid value for canTrustCertificate (%s): %s\n", strings.Join(canTrustCertificateValues.ToSlice(), "/"), canTrustCertificate) - errorCode = internal.ErrInvalidCanTrustCertificate + errorCode.Store(int32(internal.ErrInvalidCanTrustCertificate)) return } canBroadcastBattleServer := "false" if runtime.GOOS == "windows" && gameId != common.GameAoM { - canBroadcastBattleServer = viper.GetString("Config.CanBroadcastBattleServer") + canBroadcastBattleServer = cfg.Config.CanBroadcastBattleServer if !canBroadcastBattleServerValues.Contains(canBroadcastBattleServer) { logger.Printf("Invalid value for canBroadcastBattleServer (auto/false): %s\n", canBroadcastBattleServer) - errorCode = internal.ErrInvalidCanBroadcastBattleServer + errorCode.Store(int32(internal.ErrInvalidCanBroadcastBattleServer)) return } } - serverStart := viper.GetString("Server.Start") + serverStart := cfg.Server.Start if !autoTrueFalseValues.Contains(serverStart) { logger.Printf("Invalid value for serverStart (auto/true/false): %s\n", serverStart) - errorCode = internal.ErrInvalidServerStart + errorCode.Store(int32(internal.ErrInvalidServerStart)) return } - serverStop := viper.GetString("Server.Stop") + serverStop := cfg.Server.Stop if runtime.GOOS != "windows" && isAdmin { autoTrueFalseValues.Remove(falseValue) } if !autoTrueFalseValues.Contains(serverStop) { logger.Printf("Invalid value for serverStop (%s): %s\n", strings.Join(autoTrueFalseValues.ToSlice(), "/"), serverStop) - errorCode = internal.ErrInvalidServerStop + errorCode.Store(int32(internal.ErrInvalidServerStop)) return } - battleServerManagerRun := viper.GetString("Server.BattleServerManager.Run") - if !serverBattleServerManagerRunValues.Contains(battleServerManagerRun) { - logger.Printf("Invalid value for Server.BattleServerManager.Run (%s): %s\n", strings.Join(serverBattleServerManagerRunValues.ToSlice(), "/"), battleServerManagerRun) - errorCode = internal.ErrInvalidServerBattleServerManagerRun + battleServerManagerRun := cfg.Server.BattleServerManager.Run + if !requiredTrueFalseValues.Contains(battleServerManagerRun) { + logger.Printf("Invalid value for Server.BattleServerManager.Run (%s): %s\n", strings.Join(requiredTrueFalseValues.ToSlice(), "/"), battleServerManagerRun) + errorCode.Store(int32(internal.ErrInvalidServerBattleServerManagerRun)) + return + } + isolateMetadataStr := cfg.Config.IsolateMetadata + if !requiredTrueFalseValues.Contains(isolateMetadataStr) { + logger.Printf("Invalid value for Config.IsolateMetadata (%s): %s\n", strings.Join(requiredTrueFalseValues.ToSlice(), "/"), isolateMetadataStr) + errorCode.Store(int32(internal.ErrInvalidIsolateMetadata)) + return + } + isolateProfilesStr := cfg.Config.IsolateProfiles + if !requiredTrueFalseValues.Contains(isolateProfilesStr) { + logger.Printf("Invalid value for Config.IsolateProfiles (%s): %s\n", strings.Join(requiredTrueFalseValues.ToSlice(), "/"), isolateProfilesStr) + errorCode.Store(int32(internal.ErrInvalidIsolateProfiles)) return } if !common.SupportedGames.ContainsOne(gameId) { logger.Println("Invalid game type") - errorCode = launcherCommon.ErrInvalidGame + errorCode.Store(int32(launcherCommon.ErrInvalidGame)) return } config.SetGameId(gameId) @@ -137,7 +155,7 @@ var ( "Game": gameId, "Id": uuid.NewString(), } - serverArgs, err := common.ParseCommandArgs("Server.ExecutableArgs", serverValues, true) + serverArgs, err := cmdUtils.ParseCommandArgs(cfg.Server.Args, serverValues) serverId := uuid.Nil if err == nil { // Find the actual ID in case the user missed it or passed another one @@ -151,79 +169,88 @@ var ( } if serverId == uuid.Nil { logger.Println("You must provide a valid UUID for the server ID using the '--id' argument in 'server' executable arguments") - errorCode = internal.ErrInvalidServerArgs + errorCode.Store(int32(internal.ErrInvalidServerArgs)) return } } else { logger.Println("Failed to parse 'server' executable arguments") - errorCode = internal.ErrInvalidServerArgs + errorCode.Store(int32(internal.ErrInvalidServerArgs)) return } var battleServerManagerArgs []string - battleServerManagerArgs, err = common.ParseCommandArgs( - "Server.BattleServerManager.ExecutableArgs", + battleServerManagerArgs, err = cmdUtils.ParseCommandArgs( + cfg.Server.BattleServerManager.Args, serverValues, - true, ) if err != nil { logger.Println("Failed to parse 'battle-server-manager' executable arguments") - errorCode = internal.ErrInvalidServerBattleServerManagerArgs + errorCode.Store(int32(internal.ErrInvalidServerBattleServerManagerArgs)) return } var setupCommand []string - setupCommand, err = common.ParseCommandArgs("Config.SetupCommand", nil, true) + setupCommand, err = cmdUtils.ParseCommandArgs(cfg.Config.SetupCommand, nil) if err != nil { logger.Println("Failed to parse setup command") - errorCode = internal.ErrInvalidSetupCommand + errorCode.Store(int32(internal.ErrInvalidSetupCommand)) return } var revertCommand []string - revertCommand, err = common.ParseCommandArgs("Config.RevertCommand", nil, true) + revertCommand, err = cmdUtils.ParseCommandArgs(cfg.Config.RevertCommand, nil) if err != nil { logger.Println("Failed to parse revert command") - errorCode = internal.ErrInvalidRevertCommand + errorCode.Store(int32(internal.ErrInvalidRevertCommand)) return } - canAddHost := viper.GetBool("Config.CanAddHost") + canAddHost := cfg.Config.CanAddHost + var clientExecutable string + var clientExecutableOfficial bool + if clientExecutable = cfg.Client.Executable.Path; clientExecutable == "auto" || clientExecutable == "steam" || clientExecutable == "msstore" { + clientExecutableOfficial = true + } var isolateMetadata bool if gameId != common.GameAoE1 { - isolateMetadata = viper.GetBool("Config.IsolateMetadata") + isolateMetadata = cmdUtils.ResolveIsolateValue(isolateMetadataStr, clientExecutableOfficial) } - isolateProfiles := viper.GetBool("Config.IsolateProfiles") + isolateProfiles := cmdUtils.ResolveIsolateValue(isolateProfilesStr, clientExecutableOfficial) var serverExecutable string - if serverExecutable = viper.GetString("Server.Executable"); serverExecutable != "auto" { + if serverExecutable = cfg.Server.Executable.Path; serverExecutable != "auto" { var serverFile os.FileInfo - if serverFile, serverExecutable, err = common.ParsePath(viper.GetStringSlice("Server.Executable"), nil); err != nil || serverFile.IsDir() { + if serverFile, serverExecutable, err = common.ParsePath(common.EnhancedViperStringToStringSlice(cfg.Server.Executable.Path), nil); err != nil || serverFile.IsDir() { logger.Println("Invalid 'server' executable") - errorCode = internal.ErrInvalidServerPath + errorCode.Store(int32(internal.ErrInvalidServerPath)) return } } var battleServerManagerExecutable string - if battleServerManagerExecutable = viper.GetString("Server.BattleServerManager.Executable"); battleServerManagerExecutable != "auto" { + if battleServerManagerExecutable = cfg.Server.BattleServerManager.Executable.Path; battleServerManagerExecutable != "auto" { var battleServerManagerFile os.FileInfo - if battleServerManagerFile, battleServerManagerExecutable, err = common.ParsePath(viper.GetStringSlice("Server.BattleServerManager.Executable"), nil); err != nil || battleServerManagerFile.IsDir() { + if battleServerManagerFile, battleServerManagerExecutable, err = common.ParsePath(common.EnhancedViperStringToStringSlice(cfg.Server.BattleServerManager.Executable.Path), nil); err != nil || battleServerManagerFile.IsDir() { logger.Println("Invalid 'battle-server-manager' executable") - errorCode = internal.ErrInvalidClientPath + errorCode.Store(int32(internal.ErrInvalidClientPath)) return } } - var clientExecutable string - if clientExecutable = viper.GetString("Client.Executable"); clientExecutable != "auto" && clientExecutable != "steam" && clientExecutable != "msstore" { + if !clientExecutableOfficial { var clientFile os.FileInfo - if clientFile, clientExecutable, err = common.ParsePath(viper.GetStringSlice("Client.Executable"), nil); err != nil || clientFile.IsDir() { + if clientFile, clientExecutable, err = common.ParsePath(common.EnhancedViperStringToStringSlice(cfg.Client.Executable.Path), nil); err != nil || clientFile.IsDir() { logger.Println("Invalid client executable") - errorCode = internal.ErrInvalidClientPath + errorCode.Store(int32(internal.ErrInvalidClientPath)) return } + } else if !isolateProfiles || (gameId != common.GameAoE1 && !isolateMetadata) { + logger.Println("Isolating profiles and metadata is a must when using an official launcher.") + errorCode.Store(int32(internal.ErrRequiredIsolation)) + return + } else { + logger.Println("Make sure you disable the cloud saves in the launcher settings to avoid issues.") } - serverHost := viper.GetString("Server.Host") + serverHost := cfg.Server.Host logger.Printf("Game %s.\n", gameId) if clientExecutable == "msstore" && gameId == common.GameAoM { logger.Println("The Microsoft Store (Xbox) version of AoM: RT is not supported.") - errorCode = internal.ErrGameUnsupportedLauncherCombo + errorCode.Store(int32(internal.ErrGameUnsupportedLauncherCombo)) return } @@ -256,9 +283,9 @@ var ( } } if gameId != common.GameAoE1 { - if clientFile, clientPath, err := common.ParsePath(viper.GetStringSlice("Client.Path"), nil); err != nil || !clientFile.IsDir() { + if clientFile, clientPath, err := common.ParsePath(common.EnhancedViperStringToStringSlice(cfg.Client.Path), nil); err != nil || !clientFile.IsDir() { logger.Println("Invalid client path") - errorCode = internal.ErrInvalidClientPath + errorCode.Store(int32(internal.ErrInvalidClientPath)) return } else { gamePath = clientPath @@ -266,14 +293,17 @@ var ( } default: logger.Println("Game not found.") - errorCode = internal.ErrGameLauncherNotFound + errorCode.Store(int32(internal.ErrGameLauncherNotFound)) return } - if gamePath != "" && commonLogger.FileLogger != nil { + var gameCaCertPath string + if gamePath != "" { caCert := cert.NewCA(gameId, gamePath) - logger.Cacert = &caCert + gameCaCertPath = caCert.OriginalPath() + if commonLogger.FileLogger != nil { + logger.Cacert = &caCert + } } - if isAdmin { logger.Println("Running as administrator, this is not recommended for security reasons. It will request isolated admin privileges if/when needed.") if runtime.GOOS != "windows" { @@ -282,13 +312,13 @@ var ( } if runtime.GOOS != "windows" && isAdmin && (clientExecutable == "auto" || clientExecutable == "steam") { - logger.Println("Steam cannot be run as administrator. Either run this as a normal user o set Client.Executable to a custom launcher.") - errorCode = internal.ErrSteamRoot + logger.Println("Steam cannot be run as administrator. Either run this as a normal user or set Client.Executable to a custom launcher.") + errorCode.Store(int32(internal.ErrSteamRoot)) return } if cmdUtils.GameRunning() { - errorCode = internal.ErrGameAlreadyRunning + errorCode.Store(int32(internal.ErrGameAlreadyRunning)) return } @@ -302,7 +332,7 @@ var ( config.Revert() commonLogger.CloseFileLog() _ = lock.Unlock() - os.Exit(errorCode) + os.Exit(int(errorCode.Load())) } }() /* @@ -317,7 +347,7 @@ var ( commonLogger.Println("run config revert", options.String()) }, executor.RunRevert) }); err != nil { - errorCode = common.ErrFileLog + errorCode.Store(int32(common.ErrFileLog)) return } var proc *os.Process @@ -333,21 +363,21 @@ var ( logger.Println("Error message: " + err.Error()) } }); err != nil { - errorCode = common.ErrFileLog + errorCode.Store(int32(common.ErrFileLog)) return } logger.WriteFileLog(gameId, "post initial cleanup") if len(revertCommand) > 0 { if err := launcherCommon.RevertCommandStore.Store(revertCommand); err != nil { logger.Println("Failed to store revert command") - errorCode = internal.ErrInvalidRevertCommand + errorCode.Store(int32(internal.ErrInvalidRevertCommand)) return } } // Setup logger.Println("Setting up...") if len(setupCommand) > 0 { - logger.Printf("Running setup command '%s' and waiting for it to exit...\n", viper.GetString("Config.SetupCommand")) + logger.Printf("Running setup command '%s' and waiting for it to exit...\n", cfg.Config.SetupCommand) result := config.RunSetupCommand(setupCommand) if !result.Success() { if result.Err != nil { @@ -356,31 +386,32 @@ var ( if result.ExitCode != common.ErrSuccess { logger.Printf(`Exit code: %d.`+"\n", result.ExitCode) } - errorCode = internal.ErrSetupCommand + errorCode.Store(int32(internal.ErrSetupCommand)) return } } var serverIP string if serverStart == "auto" { - announcePorts := viper.GetIntSlice("Server.AnnouncePorts") + announcePorts := cfg.Server.AnnouncePorts ports := mapset.NewThreadUnsafeSetWithSize[uint16](len(announcePorts)) for _, portInt := range announcePorts { ports.Add(uint16(portInt)) } - multicastIPsStr := viper.GetStringSlice("Server.AnnounceMulticastGroups") + multicastIPsStr := cfg.Server.AnnounceMulticastGroups multicastIPs := mapset.NewThreadUnsafeSetWithSize[netip.Addr](len(multicastIPsStr)) for _, str := range multicastIPsStr { if IP, err := netip.ParseAddr(str); err == nil && IP.Is4() && IP.IsMulticast() { multicastIPs.Add(IP) } else { logger.Printf("Invalid multicast group \"%s\"\n", str) - errorCode = internal.ErrAnnouncementMulticastGroup + errorCode.Store(int32(internal.ErrAnnouncementMulticastGroup)) return } } var selectedServerIp net.IP serverId, selectedServerIp = cmdUtils.DiscoverServersAndSelectBestIpAddr( gameId, + cfg.Server.SingleAutoSelect, multicastIPs, ports, ) @@ -404,12 +435,12 @@ var ( if serverIP == "" { if serverHost == "" { logger.Println("serverStart is false. serverHost must be fulfilled as it is needed to know which host to connect to.") - errorCode = internal.ErrInvalidServerHost + errorCode.Store(int32(internal.ErrInvalidServerHost)) return } if addr, err := netip.ParseAddr(serverHost); err == nil && addr.Is6() { logger.Println("serverStart is false. serverHost must be fulfilled with a host or Ipv4 address.") - errorCode = internal.ErrInvalidServerHost + errorCode.Store(int32(internal.ErrInvalidServerHost)) return } if id, measuredServerIPAddrs, data := server.FilterServerIPs( @@ -419,7 +450,7 @@ var ( common.NetIPSliceToNetIPSet(common.StringSliceToNetIPSlice(common.HostOrIpToIps(serverHost))), ); data == nil { logger.Println("serverStart is false. Failed to resolve serverHost to a valid and reachable IP.") - errorCode = internal.ErrInvalidServerHost + errorCode.Store(int32(internal.ErrInvalidServerHost)) return } else { serverIP = measuredServerIPAddrs[0].Ip.String() @@ -445,7 +476,7 @@ var ( logger.Println("AoM: RT needs a Battle Server to be started but you don't allow to start one, make sure you have one running and the server configured.") } runBattleServerManager := battleServerManagerRun == "true" || (battleServerManagerRun == "required" && gameId == common.GameAoM) - if viper.GetString("Server.Start") == "auto" { + if cfg.Server.Start == "auto" { str := "No 'server's were found, proceeding to" if runBattleServerManager { str += " start a battle server (if needed) and then" @@ -456,69 +487,74 @@ var ( serverExecutablePath := server.GetExecutablePath(serverExecutable) if serverExecutablePath == "" { logger.Println("Cannot find 'server' executable path. Set it manually in Server.Executable.") - errorCode = internal.ErrServerExecutable + errorCode.Store(int32(internal.ErrServerExecutable)) return } if serverExecutable != serverExecutablePath { logger.Println("Found 'server' executable path:", serverExecutablePath) } - if errorCode = server.GenerateServerCertificates(serverExecutablePath, canTrustCertificate != "false"); errorCode != common.ErrSuccess { + if ec := server.GenerateServerCertificates(serverExecutablePath, canTrustCertificate != "false"); ec != common.ErrSuccess { + errorCode.Store(int32(ec)) return } if runBattleServerManager { - errorCode = config.RunBattleServerManager( + ec := config.RunBattleServerManager( battleServerManagerExecutable, battleServerManagerArgs, serverStop == "true", ) - if errorCode != common.ErrSuccess { + if ec != common.ErrSuccess { + errorCode.Store(int32(ec)) return } } - errorCode, serverIP = config.StartServer(serverExecutablePath, serverArgs, serverStop == "true", serverId) - if errorCode != common.ErrSuccess { + var ec int + ec, serverIP = config.StartServer(serverExecutablePath, serverArgs, serverStop == "true", serverId) + if ec != common.ErrSuccess { + errorCode.Store(int32(ec)) return } } serverCertificate := server.ReadCACertificateFromServer(serverIP) if serverCertificate == nil { logger.Println("Failed to read certificate from " + serverIP + ". Try to access it with your browser and checking the certificate.") - errorCode = internal.ErrReadCert + errorCode.Store(int32(internal.ErrReadCert)) return } - errorCode = config.MapHosts(gameId, serverIP, canAddHost, slices.ContainsFunc(viper.GetStringSlice("Client.ExecutableArgs"), func(s string) bool { + errorCode.Store(int32(config.MapHosts(gameId, serverIP, canAddHost, slices.ContainsFunc(cfg.Client.Args, func(s string) bool { return strings.Contains(s, "{HostFilePath}") - })) - if errorCode != common.ErrSuccess { + })))) + if errorCode.Load() != int32(common.ErrSuccess) { return } logger.WriteFileLog(gameId, "post host mapping") - errorCode = config.AddCert(gameId, serverId, serverCertificate, canTrustCertificate, slices.ContainsFunc(viper.GetStringSlice("Client.ExecutableArgs"), func(s string) bool { + errorCode.Store(int32(config.AddCert(gameId, serverId, serverCertificate, canTrustCertificate, slices.ContainsFunc(cfg.Client.Args, func(s string) bool { return strings.Contains(s, "{CertFilePath}") - })) - if errorCode != common.ErrSuccess { + })))) + if errorCode.Load() != int32(common.ErrSuccess) { return } logger.WriteFileLog(gameId, "post add cert") - errorCode = config.IsolateUserData(isolateMetadata, isolateProfiles) - if errorCode != common.ErrSuccess { + errorCode.Store(int32(config.IsolateUserData(isolateMetadata, isolateProfiles))) + if errorCode.Load() != int32(common.ErrSuccess) { return } logger.WriteFileLog(gameId, "post isolate user data") if gamePath != "" { - errorCode = config.AddCACertToGame(gameId, serverCertificate, gamePath) - if errorCode != common.ErrSuccess { + errorCode.Store(int32(config.AddCACertToGame(gameId, serverId, serverCertificate, gamePath, gameCaCertPath))) + if errorCode.Load() != int32(common.ErrSuccess) { return } logger.WriteFileLog(gameId, "post add game cert") } - errorCode = config.LaunchAgentAndGame(executer, customExecutor, canTrustCertificate, canBroadcastBattleServer) + errorCode.Store(int32(config.LaunchAgentAndGame(executer, customExecutor, cfg.Client.Args, canTrustCertificate, canBroadcastBattleServer))) }, } ) func Execute() error { rootCmd.Version = Version + // Flags rootCmd.Flags().StringVar(&cfgFile, "config", "", fmt.Sprintf(`config file (default config.toml in %s directories)`, strings.Join(configPaths, ", "))) rootCmd.Flags().StringVar(&gameCfgFile, "gameConfig", "", fmt.Sprintf(`Game config file (default config.game.toml in %s directories)`, strings.Join(configPaths, ", "))) rootCmd.Flags().Bool("log", false, "Whether to log more info to a file. Enable it for errors.") @@ -536,7 +572,7 @@ func Execute() error { if runtime.GOOS == "windows" { pathNamesInfo += " Path names need to use double backslashes within single quotes or be within double quotes." } - cmd.GameVarCommand(rootCmd.Flags(), &gameId) + commonCmd.GameVarCommand(rootCmd.Flags(), &gameId) if err := rootCmd.MarkFlagRequired("game"); err != nil { panic(err) } @@ -544,8 +580,8 @@ func Execute() error { if runtime.GOOS == "linux" { suffixIsolate = " Unsupported when using a custom launcher." } - rootCmd.Flags().StringP("isolateMetadata", "m", "true", "Isolate the metadata cache of the game, otherwise, it will be shared. Not compatible with AoE:DE."+suffixIsolate) - rootCmd.Flags().BoolP("isolateProfiles", "p", false, "(Experimental) Isolate the users profile of the game, otherwise, it will be shared."+suffixIsolate) + rootCmd.Flags().StringP("isolateMetadata", "m", "required", "Isolate the metadata cache of the game, otherwise, it will be shared. Not compatible with AoE:DE. If 'required' it will resolve to 'true' if using the official launcher, 'false' otherwise."+suffixIsolate) + rootCmd.Flags().StringP("isolateProfiles", "p", "required", "Isolate the user's profile of the game, otherwise, it will be shared. If 'required' it will resolve to 'true' if using the official launcher, 'false' otherwise."+suffixIsolate) rootCmd.Flags().String("setupCommand", "", `Executable to run (including arguments) to run first after the "Setting up..." line. The command must return a 0 exit code to continue. If you need to keep it running spawn a new separate process. You may use environment variables.`+pathNamesInfo) rootCmd.Flags().String("revertCommand", "", `Executable to run (including arguments) to run after setupCommand, game has exited and everything has been reverted. It may run before if there is an error. You may use environment variables.`+pathNamesInfo) rootCmd.Flags().StringP("serverStart", "a", "auto", `Start the 'server' if needed, "auto" will start a 'server' if one is not already running, "true" (will start a 'server' regardless if one is already running), "false" (will require an already running 'server').`) @@ -553,6 +589,7 @@ func Execute() error { rootCmd.Flags().StringSliceP("serverAnnouncePorts", "n", []string{strconv.Itoa(common.AnnouncePort)}, `Announce ports to listen to. If not including the default port, default configured 'servers' will not get discovered.`) rootCmd.Flags().StringSliceP("serverAnnounceMulticastGroups", "g", []string{common.AnnounceMulticastGroup}, `Announce multicast groups to join. If not including the default group, default configured 'servers' will not get discovered via Multicast.`) rootCmd.Flags().StringP("server", "s", "", `Hostname of the 'server' to connect to. If not absent, serverStart will be assumed to be false. Ignored otherwise`) + rootCmd.Flags().Bool("serverSingleAutoSelect", false, `Auto-select the server when a single one is discovered.`) serverExe := executables.Filename(false, executables.Server) rootCmd.Flags().StringP("serverPath", "z", "auto", fmt.Sprintf(`The executable path of the 'server', "auto", will be try to execute in this order "./%s/%s", "../%s" and finally "../%s/%s", otherwise set the path (relative or absolute).`, executables.Server, serverExe, serverExe, executables.Server, serverExe)) rootCmd.Flags().StringP("serverPathArgs", "r", "", `The arguments to pass to the 'server' executable if starting it. Execute the 'server' help flag for available arguments. You may use environment variables.`+pathNamesInfo) @@ -576,82 +613,135 @@ func Execute() error { } rootCmd.Flags().StringP("clientExe", "l", "auto", clientExeTip) rootCmd.Flags().StringP("clientExeArgs", "i", "", "The arguments to pass to the client launcher if it is custom. You may use environment variables and '{HostFilePath}'/'{CertFilePath}' replacement variables."+pathNamesInfo) - if err := viper.BindPFlag("Config.CanAddHost", rootCmd.Flags().Lookup("canAddHost")); err != nil { + // Default Values + // Config + v.SetDefault("Config.CanAddHost", "true") + v.SetDefault("Config.CanTrustCertificate", "local") + v.SetDefault("Config.CanBroadcastBattleServer", "auto") + v.SetDefault("Config.Log", false) + v.SetDefault("Config.IsolateMetadata", "required") + v.SetDefault("Config.IsolateProfiles", "required") + v.SetDefault("Config.SetupCommand", []string{}) + v.SetDefault("Config.RevertCommand", []string{}) + // Client + v.SetDefault("Client.Executable", "auto") + v.SetDefault("Client.ExecutableArgs", []string{}) + v.SetDefault("Client.Path", "auto") + // Server + v.SetDefault("Server.Start", "auto") + v.SetDefault("Server.Executable", "auto") + v.SetDefault("Server.ExecutableArgs", []string{"-e", "{Game}", "--id", "{Id}"}) + v.SetDefault("Server.Host", netip.IPv4Unspecified().String()) + v.SetDefault("Server.Stop", "auto") + v.SetDefault("Server.SingleAutoSelect", false) + v.SetDefault("Server.AnnouncePorts", []int{common.AnnouncePort}) + v.SetDefault("Server.AnnounceMulticastGroups", []string{common.AnnounceMulticastGroup}) + // Server.BattleServerManager + v.SetDefault("Server.BattleServerManager.Run", "true") + v.SetDefault("Server.BattleServerManager.Executable", "auto") + v.SetDefault("Server.BattleServerManager.ExecutableArgs", []string{"-e", "{Game}", "-r"}) + // Bindings + if err := v.BindPFlag("Config.CanAddHost", rootCmd.Flags().Lookup("canAddHost")); err != nil { return err } - if err := viper.BindPFlag("Config.CanTrustCertificate", rootCmd.Flags().Lookup("canTrustCertificate")); err != nil { + if err := v.BindPFlag("Config.CanTrustCertificate", rootCmd.Flags().Lookup("canTrustCertificate")); err != nil { return err } if runtime.GOOS == "windows" { - if err := viper.BindPFlag("Config.CanBroadcastBattleServer", rootCmd.Flags().Lookup("canBroadcastBattleServer")); err != nil { + if err := v.BindPFlag("Config.CanBroadcastBattleServer", rootCmd.Flags().Lookup("canBroadcastBattleServer")); err != nil { return err } } - if err := viper.BindPFlag("Config.Log", rootCmd.Flags().Lookup("log")); err != nil { + if err := v.BindPFlag("Config.Log", rootCmd.Flags().Lookup("log")); err != nil { return err } - if err := viper.BindPFlag("Config.IsolateMetadata", rootCmd.Flags().Lookup("isolateMetadata")); err != nil { + if err := v.BindPFlag("Config.IsolateMetadata", rootCmd.Flags().Lookup("isolateMetadata")); err != nil { return err } - if err := viper.BindPFlag("Config.IsolateProfiles", rootCmd.Flags().Lookup("isolateProfiles")); err != nil { + if err := v.BindPFlag("Config.IsolateProfiles", rootCmd.Flags().Lookup("isolateProfiles")); err != nil { return err } - if err := viper.BindPFlag("Config.SetupCommand", rootCmd.Flags().Lookup("setupCommand")); err != nil { + if err := v.BindPFlag("Config.SetupCommand", rootCmd.Flags().Lookup("setupCommand")); err != nil { return err } - if err := viper.BindPFlag("Config.RevertCommand", rootCmd.Flags().Lookup("revertCommand")); err != nil { + if err := v.BindPFlag("Config.RevertCommand", rootCmd.Flags().Lookup("revertCommand")); err != nil { return err } - if err := viper.BindPFlag("Server.Start", rootCmd.Flags().Lookup("serverStart")); err != nil { + if err := v.BindPFlag("Server.Start", rootCmd.Flags().Lookup("serverStart")); err != nil { return err } - if err := viper.BindPFlag("Server.Stop", rootCmd.Flags().Lookup("serverStop")); err != nil { + if err := v.BindPFlag("Server.Stop", rootCmd.Flags().Lookup("serverStop")); err != nil { return err } - if err := viper.BindPFlag("Server.AnnouncePorts", rootCmd.Flags().Lookup("serverAnnouncePorts")); err != nil { + if err := v.BindPFlag("Server.SingleAutoSelect", rootCmd.Flags().Lookup("serverSingleAutoSelect")); err != nil { return err } - if err := viper.BindPFlag("Server.AnnounceMulticastGroups", rootCmd.Flags().Lookup("serverAnnounceMulticastGroups")); err != nil { + if err := v.BindPFlag("Server.AnnouncePorts", rootCmd.Flags().Lookup("serverAnnouncePorts")); err != nil { return err } - if err := viper.BindPFlag("Server.Host", rootCmd.Flags().Lookup("server")); err != nil { + if err := v.BindPFlag("Server.AnnounceMulticastGroups", rootCmd.Flags().Lookup("serverAnnounceMulticastGroups")); err != nil { return err } - if err := viper.BindPFlag("Server.Executable", rootCmd.Flags().Lookup("serverPath")); err != nil { + if err := v.BindPFlag("Server.Host", rootCmd.Flags().Lookup("server")); err != nil { return err } - if err := viper.BindPFlag("Server.ExecutableArgs", rootCmd.Flags().Lookup("serverPathArgs")); err != nil { + if err := v.BindPFlag("Server.Executable", rootCmd.Flags().Lookup("serverPath")); err != nil { return err } - if err := viper.BindPFlag("Client.Executable", rootCmd.Flags().Lookup("clientExe")); err != nil { + if err := v.BindPFlag("Server.ExecutableArgs", rootCmd.Flags().Lookup("serverPathArgs")); err != nil { return err } - if err := viper.BindPFlag("Client.ExecutableArgs", rootCmd.Flags().Lookup("clientExeArgs")); err != nil { + if err := v.BindPFlag("Client.Executable", rootCmd.Flags().Lookup("clientExe")); err != nil { + return err + } + if err := v.BindPFlag("Client.ExecutableArgs", rootCmd.Flags().Lookup("clientExeArgs")); err != nil { return err } return rootCmd.Execute() } -func initConfig() { +func initConfig() *internal.Configuration { for _, configPath := range configPaths { - viper.AddConfigPath(configPath) + v.AddConfigPath(configPath) } - viper.SetConfigType("toml") + v.SetConfigType("toml") if cfgFile != "" { - viper.SetConfigFile(cfgFile) + v.SetConfigFile(cfgFile) } else { - viper.SetConfigName("config") + v.SetConfigName("config") } - if err := viper.MergeInConfig(); err == nil { - logger.Println("Using main config file:", viper.ConfigFileUsed()) + if err := v.MergeInConfig(); err == nil { + logger.Println("Using main config file:", v.ConfigFileUsed()) + logger.PrintFile("main config", v.ConfigFileUsed()) + } else { + var configFileNotFoundError viper.ConfigFileNotFoundError + if !errors.As(err, &configFileNotFoundError) { + logger.Println("Error parsing config file:", v.ConfigFileUsed()+":", err.Error()) + os.Exit(common.ErrConfigParse) + } } if gameCfgFile != "" { - viper.SetConfigFile(gameCfgFile) + v.SetConfigFile(gameCfgFile) + } else { + v.SetConfigName(fmt.Sprintf("config.%s", gameId)) + } + if err := v.MergeInConfig(); err == nil { + logger.Println("Using game config file:", v.ConfigFileUsed()) + logger.PrintFile("game config", v.ConfigFileUsed()) } else { - viper.SetConfigName(fmt.Sprintf("config.%s", gameId)) + var configFileNotFoundError viper.ConfigFileNotFoundError + if !errors.As(err, &configFileNotFoundError) { + logger.Println("Error parsing game config file:", v.ConfigFileUsed()+":", err.Error()) + os.Exit(internal.ErrGameConfigParse) + } } - if err := viper.MergeInConfig(); err == nil { - logger.Println("Using game config file:", viper.ConfigFileUsed()) + v.AutomaticEnv() + viper.ExperimentalBindStruct() + var c *internal.Configuration + err := v.Unmarshal(&c) + if err != nil { + logger.Println("unable to decode configuration:", err.Error()) + os.Exit(common.ErrConfigParse) } - viper.AutomaticEnv() + return c } diff --git a/launcher/internal/cmdUtils/caCert.go b/launcher/internal/cmdUtils/caCert.go index 5cf952e8..194b43f6 100644 --- a/launcher/internal/cmdUtils/caCert.go +++ b/launcher/internal/cmdUtils/caCert.go @@ -4,19 +4,43 @@ import ( "crypto/x509" "io" + "github.com/google/uuid" "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/executor/exec" commonLogger "github.com/luskaner/ageLANServer/common/logger" + launcherCommonCert "github.com/luskaner/ageLANServer/launcher-common/cert" "github.com/luskaner/ageLANServer/launcher/internal" "github.com/luskaner/ageLANServer/launcher/internal/cmdUtils/logger" "github.com/luskaner/ageLANServer/launcher/internal/executor" ) -func (c *Config) AddCACertToGame(gameId string, cert *x509.Certificate, gamePath string) (errorCode int) { - logger.Println("Adding CA certificate to game...") - var err error +func readCertsPool(path string) (pool *x509.CertPool, err error) { + var caCerts []*x509.Certificate + _, _, caCerts, err = launcherCommonCert.ReadFromFile(path) + if err != nil { + return + } + pool = x509.NewCertPool() + for _, caCert := range caCerts { + pool.AddCert(caCert) + } + return +} + +func (c *Config) AddCACertToGame(gameId string, serverId uuid.UUID, serverCertificate *x509.Certificate, gamePath string, caCertPath string) (errorCode int) { + logger.Println("Adding CA certificate to game if needed...") + caPool, err := readCertsPool(caCertPath) + if err != nil { + logger.Println("Could not read game CA certificates:", err) + return common.ErrSuccess + } + var addCert bool + addCert, errorCode = checkCertMatch(serverId, gameId, serverCertificate, common.AllHosts(gameId), caPool, true) + if !addCert || errorCode != common.ErrSuccess { + return + } if err = commonLogger.FileLogger.Buffer("config_setup_CA_game", func(writer io.Writer) { - if result := executor.RunSetUp(gameId, nil, nil, nil, cert.Raw, false, false, false, "", "", gamePath, writer, func(options exec.Options) { + if result := executor.RunSetUp(gameId, nil, nil, nil, serverCertificate.Raw, false, false, false, "", "", gamePath, writer, func(options exec.Options) { commonLogger.Println("run config setup for CA game cert", options.String()) }); !result.Success() { logger.Println("Failed to save CA certificate to game") diff --git a/launcher/internal/cmdUtils/cert.go b/launcher/internal/cmdUtils/cert.go index 049c7207..04f9cb4f 100644 --- a/launcher/internal/cmdUtils/cert.go +++ b/launcher/internal/cmdUtils/cert.go @@ -19,40 +19,48 @@ import ( "github.com/luskaner/ageLANServer/launcher/internal/server" ) +func checkCertMatch(serverId uuid.UUID, gameId string, serverCertificate *x509.Certificate, hosts []string, rootCAs *x509.CertPool, fixable bool) (requiresFixing bool, errorCode int) { + for _, host := range hosts { + if !server.CheckConnectionFromServer(host, false, rootCAs) { + if fixable { + cert := server.ReadCACertificateFromServer(host) + if cert == nil { + logger.Println("Failed to read certificate from " + host + ".") + errorCode = internal.ErrReadCert + return + } else if !bytes.Equal(cert.Raw, serverCertificate.Raw) { + logger.Println("The certificate for " + host + " does not match the server certificate.") + errorCode = internal.ErrCertMismatch + return + } + requiresFixing = true + } else { + logger.Println(host + " must have been trusted manually. If you want it automatically, set config/option CanTrustCertificate to 'user' or 'local'.") + errorCode = internal.ErrConfigCert + return + } + } else if cert := server.ReadCACertificateFromServer(host); cert == nil || !bytes.Equal(cert.Raw, serverCertificate.Raw) { + logger.Println("The certificate for " + host + " does not match the server certificate (or could not be read).") + errorCode = internal.ErrCertMismatch + return + } else if !server.LanServerHost(serverId, gameId, host, false, rootCAs) { + logger.Println("Something went wrong, " + host + " does not point to a lan server.") + errorCode = internal.ErrServerConnectSecure + return + } + } + return +} + func (c *Config) AddCert(gameId string, serverId uuid.UUID, serverCertificate *x509.Certificate, canAdd string, customCertFile bool) (errorCode int) { hosts := common.AllHosts(gameId) var addCert bool if customCertFile { addCert = true } else { - for _, host := range hosts { - if !server.CheckConnectionFromServer(host, false) { - if canAdd != "false" { - cert := server.ReadCACertificateFromServer(host) - if cert == nil { - logger.Println("Failed to read certificate from " + host + ".") - errorCode = internal.ErrReadCert - return - } else if !bytes.Equal(cert.Raw, serverCertificate.Raw) { - logger.Println("The certificate for " + host + " does not match the server certificate.") - errorCode = internal.ErrCertMismatch - return - } - addCert = true - } else { - logger.Println(host + " must have been trusted manually. If you want it automatically, set config/option CanTrustCertificate to 'user' or 'local'.") - errorCode = internal.ErrConfigCert - return - } - } else if cert := server.ReadCACertificateFromServer(host); cert == nil || !bytes.Equal(cert.Raw, serverCertificate.Raw) { - logger.Println("The certificate for " + host + " does not match the server certificate (or could not be read).") - errorCode = internal.ErrCertMismatch - return - } else if !server.LanServerHost(serverId, gameId, host, false) { - logger.Println("Something went wrong, " + host + " does not point to a lan server.") - errorCode = internal.ErrServerConnectSecure - return - } + addCert, errorCode = checkCertMatch(serverId, gameId, serverCertificate, hosts, nil, canAdd != "false") + if errorCode != common.ErrSuccess { + return } } if !addCert { @@ -113,11 +121,11 @@ func (c *Config) AddCert(gameId string, serverId uuid.UUID, serverCertificate *x } if !customCertFile { for _, host := range hosts { - if !server.CheckConnectionFromServer(host, false) { + if !server.CheckConnectionFromServer(host, false, nil) { logger.Println(host + " must have been trusted automatically at this point.") errorCode = internal.ErrServerConnectSecure return - } else if !server.LanServerHost(serverId, gameId, host, false) { + } else if !server.LanServerHost(serverId, gameId, host, false, nil) { logger.Println("Something went wrong, " + host + " either points to the original 'server' or there is a certificate issue.") errorCode = internal.ErrTrustCert return diff --git a/launcher/internal/cmdUtils/game.go b/launcher/internal/cmdUtils/game.go index faac50f1..42619dd7 100644 --- a/launcher/internal/cmdUtils/game.go +++ b/launcher/internal/cmdUtils/game.go @@ -27,7 +27,7 @@ func (c *Config) KillAgent() { } } -func (c *Config) LaunchAgentAndGame(executer gameExecutor.Exec, customExecutor gameExecutor.CustomExec, canTrustCertificate string, canBroadcastBattleServer string) (errorCode int) { +func (c *Config) LaunchAgentAndGame(executer gameExecutor.Exec, customExecutor gameExecutor.CustomExec, clientExecutableArgs []string, canTrustCertificate string, canBroadcastBattleServer string) (errorCode int) { if canBroadcastBattleServer != "false" { if battleServerBroadcast.Required() { canBroadcastBattleServer = "true" @@ -78,9 +78,9 @@ func (c *Config) LaunchAgentAndGame(executer gameExecutor.Exec, customExecutor g logger.Printf(`Exit code: %d.`+"\n", result.ExitCode) } return - } else { - logger.Println("'Agent' started.") } + + logger.Println("'Agent' started.") } str := "Starting game" if customExecutor.Executable != "" { @@ -106,7 +106,7 @@ func (c *Config) LaunchAgentAndGame(executer gameExecutor.Exec, customExecutor g values["CertFilePath"] = strings.ReplaceAll(c.certFilePath, `\`, `\\`) } } - args, err := ParseCommandArgs("Client.ExecutableArgs", values) + args, err := ParseCommandArgs(clientExecutableArgs, values) if err != nil { logger.Println("Failed to parse client executable arguments") errorCode = internal.ErrInvalidClientArgs diff --git a/launcher/internal/cmdUtils/hosts.go b/launcher/internal/cmdUtils/hosts.go index cd96147d..f5951c70 100644 --- a/launcher/internal/cmdUtils/hosts.go +++ b/launcher/internal/cmdUtils/hosts.go @@ -29,10 +29,10 @@ func (c *Config) MapHosts(gameId string, ip string, canMap bool, customHostFile logger.Println("serverStart is false and canAddHost is false but 'server' does not match " + domain + ". You should have added the host ip mapping to it in the hosts file (or just set canAddHost to true).") errorCode = internal.ErrConfigIpMap return - } else { - mapIP = true } - } else if !server.CheckConnectionFromServer(domain, true) { + + mapIP = true + } else if !server.CheckConnectionFromServer(domain, true, nil) { logger.Println("serverStart is false and host matches. " + domain + " must be reachable. Review the host is reachable via this domain to TCP port 443 (HTTPS).") errorCode = internal.ErrServerUnreachable return diff --git a/launcher/internal/cmdUtils/isolateUserData.go b/launcher/internal/cmdUtils/isolateUserData.go index 30916245..1fb7948a 100644 --- a/launcher/internal/cmdUtils/isolateUserData.go +++ b/launcher/internal/cmdUtils/isolateUserData.go @@ -12,6 +12,19 @@ import ( "github.com/luskaner/ageLANServer/launcher/internal/executor" ) +func ResolveIsolateValue(value string, officialLauncher bool) bool { + switch value { + case "true": + return true + case "false": + return false + case "required": + return officialLauncher + default: + return false + } +} + func (c *Config) IsolateUserData(metadata bool, profiles bool) (errorCode int) { if metadata || profiles { var isolateItems []string diff --git a/launcher/internal/cmdUtils/logger/log.go b/launcher/internal/cmdUtils/logger/log.go index eb0e1949..3afc05b5 100644 --- a/launcher/internal/cmdUtils/logger/log.go +++ b/launcher/internal/cmdUtils/logger/log.go @@ -11,18 +11,18 @@ import ( mapset "github.com/deckarep/golang-set/v2" "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/executables" - "github.com/luskaner/ageLANServer/common/logger" + commonLogger "github.com/luskaner/ageLANServer/common/logger" "github.com/luskaner/ageLANServer/common/process" launcherCommon "github.com/luskaner/ageLANServer/launcher-common" "github.com/luskaner/ageLANServer/launcher-common/cert" "github.com/luskaner/ageLANServer/launcher-common/hosts" "github.com/luskaner/ageLANServer/launcher-common/userData" - "github.com/spf13/viper" ) var processesLog = []string{executables.LauncherAgent, executables.LauncherConfigAdminAgent} var allHosts []string var Cacert *cert.CA +var LogEnabled bool var dataTypeToString = map[int]string{ userData.TypeServer: "Own Backup", userData.TypeBackup: "Original Backup", @@ -30,7 +30,7 @@ var dataTypeToString = map[int]string{ } func OpenMainFileLog(gameId string) error { - if viper.GetBool("Config.Log") { + if LogEnabled { err := commonLogger.NewOwnFileLogger("launcher", "", gameId, false) if err != nil { return err @@ -74,6 +74,13 @@ func WriteFileLog(gameId string, name string) { } } +func PrintFile(name string, path string) { + if commonLogger.FileLogger != nil { + data, _ := os.ReadFile(path) + commonLogger.PrefixPrintln(name, string(data)) + } +} + func Printf(format string, a ...any) { commonLogger.PrefixPrintf("main", format, a...) fmt.Printf(format, a...) @@ -115,7 +122,7 @@ func writeHostInfo(_ string) error { hostsSet.Add(strings.ToLower(host)) } if hostsSet.ContainsAnyElement(allHostsSet) { - commonLogger.Printf(line.String()) + commonLogger.Printf("%s", line.String()) addedSomeEntry = true } } diff --git a/launcher/internal/cmdUtils/parse.go b/launcher/internal/cmdUtils/parse.go index 467ddf30..21a74bbd 100644 --- a/launcher/internal/cmdUtils/parse.go +++ b/launcher/internal/cmdUtils/parse.go @@ -1,25 +1,9 @@ package cmdUtils import ( - "fmt" - "regexp" - "runtime" - "strings" - - "github.com/spf13/viper" - "mvdan.cc/sh/v3/shell" + "github.com/luskaner/ageLANServer/common" ) -var reWinToLinVar = regexp.MustCompile(`%(\w+)%`) - -func ParseCommandArgs(name string, values map[string]string) (args []string, err error) { - cmdArgs := strings.Join(viper.GetStringSlice(name), " ") - for key, value := range values { - cmdArgs = strings.ReplaceAll(cmdArgs, fmt.Sprintf(`{%s}`, key), value) - } - if runtime.GOOS == "windows" { - cmdArgs = reWinToLinVar.ReplaceAllString(cmdArgs, `$$$1`) - } - args, err = shell.Fields(cmdArgs, nil) - return +func ParseCommandArgs(cmdSlice []string, values map[string]string) (args []string, err error) { + return common.ParseCommandArgsFromSlice(cmdSlice, values, true) } diff --git a/launcher/internal/cmdUtils/server.go b/launcher/internal/cmdUtils/server.go index 4ad19d2d..2ddaf7af 100644 --- a/launcher/internal/cmdUtils/server.go +++ b/launcher/internal/cmdUtils/server.go @@ -86,7 +86,7 @@ func processedServers(gameTitle string, servers map[uuid.UUID]*server.AnnounceMe return processed } -func DiscoverServersAndSelectBestIpAddr(gameTitle string, multicastGroups mapset.Set[netip.Addr], targetPorts mapset.Set[uint16]) (id uuid.UUID, ip net.IP) { +func DiscoverServersAndSelectBestIpAddr(gameTitle string, singleAutoSelect bool, multicastGroups mapset.Set[netip.Addr], targetPorts mapset.Set[uint16]) (id uuid.UUID, ip net.IP) { id = uuid.Nil servers := make(map[uuid.UUID]*server.AnnounceMessage) logger.Println("Looking for 'server's, you might need to allow the 'launcher' in the firewall...") @@ -99,11 +99,16 @@ func DiscoverServersAndSelectBestIpAddr(gameTitle string, multicastGroups mapset for i := range procServers { logger.Printf("%d. %s\n", i+1, procServers[i].description) } - logger.Printf("Enter the number of the 'server' (1-%d): ", len(procServers)) - _, err := fmt.Scan(&option) - if err != nil || option < 1 || option > len(procServers) { - logger.Println("Invalid (or error reading) option. Please enter a number from the list.") - continue + if singleAutoSelect && len(procServers) == 1 { + logger.Println("Auto-selecting the only found 'server'.") + option = 1 + } else { + logger.Printf("Enter the number of the 'server' (1-%d): ", len(procServers)) + _, err := fmt.Scan(&option) + if err != nil || option < 1 || option > len(procServers) { + logger.Println("Invalid (or error reading) option. Please enter a number from the list.") + continue + } } selectedServer := procServers[option-1] ip = selectedServer.Ip diff --git a/launcher/internal/config.go b/launcher/internal/config.go new file mode 100644 index 00000000..fd45ba8b --- /dev/null +++ b/launcher/internal/config.go @@ -0,0 +1,44 @@ +package internal + +type Executable struct { + Path string `mapstructure:"Executable"` + Args []string `mapstructure:"ExecutableArgs"` +} + +type Config struct { + CanAddHost bool + CanTrustCertificate string + CanBroadcastBattleServer string + Log bool + IsolateMetadata string + IsolateProfiles string + SetupCommand []string + RevertCommand []string +} + +type BattleServerManager struct { + Executable `mapstructure:",squash"` + Run string +} + +type Server struct { + Executable `mapstructure:",squash"` + Start string + Host string + Stop string + SingleAutoSelect bool + AnnouncePorts []int + AnnounceMulticastGroups []string + BattleServerManager BattleServerManager +} + +type Client struct { + Executable `mapstructure:",squash"` + Path string +} + +type Configuration struct { + Config Config + Server Server + Client Client +} diff --git a/launcher/internal/errors.go b/launcher/internal/errors.go index ff2e450e..b008f258 100644 --- a/launcher/internal/errors.go +++ b/launcher/internal/errors.go @@ -43,4 +43,8 @@ const ( ErrInvalidServerBattleServerManagerRun ErrInvalidServerBattleServerManagerArgs ErrBattleServerManagerRun + ErrInvalidIsolateMetadata + ErrInvalidIsolateProfiles + ErrRequiredIsolation + ErrGameConfigParse ) diff --git a/launcher/internal/server/server.go b/launcher/internal/server/server.go index 8cae37c9..5e0c5f6a 100644 --- a/launcher/internal/server/server.go +++ b/launcher/internal/server/server.go @@ -1,6 +1,7 @@ package server import ( + "crypto/x509" "encoding/json" "io" "net" @@ -92,11 +93,13 @@ func GenerateServerCertificates(serverExecutablePath string, canTrustCertificate }); !result.Success() { logger.Println("Failed to generate certificate pair. Check the folder and its permissions") errorCode = internal.ErrServerCertCreate - if result.Err != nil { - logger.Println("Error message: " + result.Err.Error()) - } - if result.ExitCode != common.ErrSuccess { - logger.Printf(`Exit code: %d.`+"\n", result.ExitCode) + if result != nil { + if result.Err != nil { + logger.Println("Error message: " + result.Err.Error()) + } + if result.ExitCode != common.ErrSuccess { + logger.Printf(`Exit code: %d.`+"\n", result.ExitCode) + } } return } @@ -111,13 +114,13 @@ func GetExecutablePath(executable string) string { return executable } -func LanServerHost(id uuid.UUID, gameTitle string, host string, insecureSkipVerify bool) (ok bool) { - ipAddrs := common.HostToIps(host) +func LanServerHost(id uuid.UUID, gameTitle string, host string, insecureSkipVerify bool, rootCAs *x509.CertPool) (ok bool) { + ipAddrs := common.HostOrIpToIps(host) if len(ipAddrs) == 0 { return } for _, ipAddr := range ipAddrs { - if ok, _, _, _ = lanServerIP(id, gameTitle, ipAddr, host, insecureSkipVerify, true); !ok { + if ok, _, _, _ = lanServerIP(id, gameTitle, net.ParseIP(ipAddr), host, insecureSkipVerify, rootCAs, true); !ok { return } } @@ -130,7 +133,7 @@ func FilterServerIPs(id uuid.UUID, serverName string, gameTitle string, possible var ok bool var latency time.Duration var tmpData *AnnounceMessageDataSupportedLatest - if ok, actualId, latency, tmpData = lanServerIP(id, gameTitle, ip, serverName, true, false); ok { + if ok, actualId, latency, tmpData = lanServerIP(id, gameTitle, ip, serverName, true, nil, false); ok { measuredIpAddresses = append(measuredIpAddresses, MesuredIpAddress{ Ip: ip, Latency: latency, @@ -146,9 +149,9 @@ func FilterServerIPs(id uuid.UUID, serverName string, gameTitle string, possible return } -func lanServerIP(id uuid.UUID, gameTitle string, ipAddr net.IP, serverName string, insecureSkipVerify bool, ignoreLatency bool) (ok bool, serverId uuid.UUID, latency time.Duration, data *AnnounceMessageDataSupportedLatest) { +func lanServerIP(id uuid.UUID, gameTitle string, ipAddr net.IP, serverName string, insecureSkipVerify bool, rootCAs *x509.CertPool, ignoreLatency bool) (ok bool, serverId uuid.UUID, latency time.Duration, data *AnnounceMessageDataSupportedLatest) { tr := &http.Transport{ - TLSClientConfig: TlsConfig(serverName, insecureSkipVerify), + TLSClientConfig: TlsConfig(serverName, insecureSkipVerify, rootCAs), } client := &http.Client{Transport: tr, Timeout: 1 * time.Second} u := url.URL{ @@ -165,7 +168,8 @@ func lanServerIP(id uuid.UUID, gameTitle string, ipAddr net.IP, serverName strin return } req.Host = serverName - if _, err = client.Do(req); err != nil { + if //goland:noinspection ALL + _, err = client.Do(req); err != nil { return } latencyAccumulator += time.Since(start) @@ -237,7 +241,9 @@ func QueryServers( target *net.UDPAddr } var connTargets []*connTarget + var conns []*net.UDPConn for source, targets := range sourceToTargetAddrs { + //goland:noinspection GoResourceLeak conn, err := net.ListenUDP( "udp4", source, @@ -250,9 +256,11 @@ func QueryServers( }) { p := ipv4.NewPacketConn(conn) if err = p.SetMulticastLoopback(true); err != nil { + _ = conn.Close() continue } } + conns = append(conns, conn) for _, target := range targets { connTargets = append(connTargets, &connTarget{ conn: conn, @@ -261,16 +269,16 @@ func QueryServers( } } + defer func(connections []*net.UDPConn) { + for _, conn := range connections { + _ = conn.Close() + } + }(conns) + if len(connTargets) == 0 { return } - defer func(connectionPairs []*connTarget) { - for _, connPair := range connectionPairs { - _ = connPair.conn.Close() - } - }(connTargets) - data := []byte(common.AnnounceHeader) var serverLock sync.Mutex @@ -313,19 +321,16 @@ func QueryServers( var wg sync.WaitGroup for _, conn := range connTargets { - wg.Add(1) - go func(conn *connTarget) { + wg.Go(func() { + packetBuffer := make([]byte, len(common.AnnounceHeader)+AnnounceIdLength) + sendAndReceive(&packetBuffer, conn, servers) ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() - packetBuffer := make([]byte, len(common.AnnounceHeader)+AnnounceIdLength) - for i := 0; i < 3; i++ { - select { - case <-ticker.C: - sendAndReceive(&packetBuffer, conn, servers) - } + for i := 0; i < 2; i++ { + <-ticker.C + sendAndReceive(&packetBuffer, conn, servers) } - wg.Done() - }(conn) + }) } wg.Wait() } diff --git a/launcher/internal/server/ssl.go b/launcher/internal/server/ssl.go index fa272b2d..bb270e20 100644 --- a/launcher/internal/server/ssl.go +++ b/launcher/internal/server/ssl.go @@ -15,16 +15,18 @@ import ( "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/executables" "github.com/luskaner/ageLANServer/common/executor/exec" + commonLogger "github.com/luskaner/ageLANServer/common/logger" ) -func TlsConfig(serverName string, insecureSkipVerify bool) *tls.Config { +func TlsConfig(serverName string, insecureSkipVerify bool, rootCAs *x509.CertPool) *tls.Config { return &tls.Config{ InsecureSkipVerify: insecureSkipVerify, ServerName: serverName, + RootCAs: rootCAs, } } -func connectToServer(host string, insecureSkipVerify bool) *tls.Conn { +func connectToServer(host string, insecureSkipVerify bool, rootCAs *x509.CertPool) *tls.Conn { ips := common.HostOrIpToIps(host) var ip string if len(ips) == 0 { @@ -32,15 +34,15 @@ func connectToServer(host string, insecureSkipVerify bool) *tls.Conn { } else { ip = ips[0] } - conn, err := tls.Dial("tcp4", net.JoinHostPort(ip, "443"), TlsConfig(host, insecureSkipVerify)) + conn, err := tls.Dial("tcp4", net.JoinHostPort(ip, "443"), TlsConfig(host, insecureSkipVerify, rootCAs)) if err != nil { return nil } return conn } -func CheckConnectionFromServer(host string, insecureSkipVerify bool) bool { - conn := connectToServer(host, insecureSkipVerify) +func CheckConnectionFromServer(host string, insecureSkipVerify bool, rootCAs *x509.CertPool) bool { + conn := connectToServer(host, insecureSkipVerify, rootCAs) if conn == nil { return false } @@ -52,7 +54,7 @@ func CheckConnectionFromServer(host string, insecureSkipVerify bool) bool { func ReadCACertificateFromServer(host string) *x509.Certificate { tr := &http.Transport{ - TLSClientConfig: TlsConfig(host, true), + TLSClientConfig: TlsConfig(host, true, nil), } ips := common.HostOrIpToIps(host) var ip string @@ -62,12 +64,19 @@ func ReadCACertificateFromServer(host string) *x509.Certificate { ip = ips[0] } client := &http.Client{Transport: tr} + //goland:noinspection ALL resp, err := client.Get(fmt.Sprintf("https://%s/cacert.pem", ip)) - if err != nil || resp.StatusCode != http.StatusOK { + if err != nil { + commonLogger.Println("ReadCACertificateFromServer error:", err) + return nil + } + if resp.StatusCode != http.StatusOK { + commonLogger.Println("ReadCACertificateFromServer status code:", resp.StatusCode) return nil } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { + commonLogger.Println("ReadCACertificateFromServer read error:", err) return nil } defer func(Body io.ReadCloser) { @@ -75,10 +84,12 @@ func ReadCACertificateFromServer(host string) *x509.Certificate { }(resp.Body) block, _ := pem.Decode(bodyBytes) if block == nil || block.Type != "CERTIFICATE" { + commonLogger.Println("ReadCACertificateFromServer: no certificate found") return nil } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { + commonLogger.Println("ReadCACertificateFromServer parse error:", err) return nil } return cert diff --git a/launcher/resources/config.game.toml b/launcher/resources/config.game.toml index d045db5a..919e9208 100644 --- a/launcher/resources/config.game.toml +++ b/launcher/resources/config.game.toml @@ -1,4 +1,14 @@ [Config] +# If 'required' it will resolve to 'true' if using the official launcher, 'false' otherwise. +# If it resolves to 'true', the launcher will isolate the metadata cache of the game, otherwise, it will be shared +# On AoE I this is always false as it does not store metadata. +# Linux Note: Only supported when not using a custom launcher. +IsolateMetadata = 'required' +# If 'required' it will resolve to 'true' if using the official launcher, 'false' otherwise. +# If it resolves to 'true', the launcher will isolate user profiles for the game, otherwise, it will be shared +# May cause issues with Steam Cloud or Xbox Cloud (depending on the game version) unless you disable cloud sync. +# Linux Note: Only supported when not using a custom launcher. +IsolateProfiles = 'required' # Executable to run (including arguments) to run first after the "Setting up..." line. # The command must return a 0 exit code to continue. If you need to keep it running spawn a new detached process. # Windows: Path names need to use double backslashes within single quotes or be within double quotes. @@ -12,6 +22,7 @@ RevertCommand = [] [Client] # The path to the game launcher, if 'auto', the Steam and then the Xbox (Windows-only) one will be launched if found # Use a path to the game launcher, 'steam' or 'msstore' (non AoM: RT and Windows-only) to use the default launcher. +# When using an official launcher, Config.IsolateMetadata and Config.IsolateProfiles must be 'required' or 'true'. # Note: It is not recommended to use an executable that requires admin rights (but it is supported). # Linux Note: setting a custom launcher will make 'Config.IsolateMetadata' and 'Config.IsolateProfiles' act as false. # Note 2: You may use environment variables. diff --git a/launcher/resources/config.toml b/launcher/resources/config.toml index 8fee8686..006c5cfb 100644 --- a/launcher/resources/config.toml +++ b/launcher/resources/config.toml @@ -1,5 +1,5 @@ [Config] -# Whether or not to add a local dns entry if it's needed to connect to the server with the official domain. +# Whether to add a local dns entry if it's needed to connect to the server with the official domain. # Including to avoid receiving that it's on maintenance. # If using '{HostFilePath}' in 'Client.ExecutableArgs', this will be ignored. # Note: Will require admin privileges. @@ -10,17 +10,9 @@ CanAddHost = true # user: trust the certificate in the user store. Requires user consent. Only on Windows. # If using '{CertFilePath}' in 'Client.ExecutableArgs', this will be ignored. CanTrustCertificate = 'local' -# Whether or not to broadcast the game BattleServer to all interfaces in LAN (not just the most priority one). +# Whether to broadcast the game BattleServer to all interfaces in LAN (not just the most priority one). # Either 'auto' or 'false'. This is only necessary in Windows when not running AoM, on the rest of systems it is always 'false'. CanBroadcastBattleServer = 'auto' -# If true, the launcher will isolate the metadata cache of the game, if false, it will be shared -# On AoE I this is always false as it does not store metadata. -# Linux Note: Only supported when not using a custom launcher. -IsolateMetadata = true -# (Experimental) If true, the launcher will isolate the user profiles of the game, if false, it will be shared. -# May cause issues with Steam Cloud or Xbox Cloud (depending on the game version) unless you disable cloud sync. -# Linux Note: Only supported when not using a custom launcher. -IsolateProfiles = false # Whether to log all the info the terminal plus more info to a file. Enable it for errors. Log = false @@ -48,6 +40,9 @@ Host = '0.0.0.0' # Whether to stop it automatically, if 'auto', it will stop the server if Start = true (or 'auto' does not find a server) # This will also stop the Battle Server is one was started with the Battle Server Manager. Stop = 'auto' +# Whether to select automatically the server if only one was discovered. +# Not recommended to be set to true if users connect using the official game launcher. +SingleAutoSelect = false # Announce ports to listen to. If not including the default port, default configured servers will not get discovered. AnnouncePorts = [31978] # Announce multicast group adresses to join. diff --git a/server-genCert/go.mod b/server-genCert/go.mod index 6bfaaab5..5a3d52aa 100644 --- a/server-genCert/go.mod +++ b/server-genCert/go.mod @@ -1,6 +1,6 @@ module github.com/luskaner/ageLANServer/server-genCert -go 1.24.0 +go 1.25.5 require github.com/spf13/cobra v1.10.2 diff --git a/server-genCert/go.sum b/server-genCert/go.sum index ef5d78dd..faf154c2 100644 --- a/server-genCert/go.sum +++ b/server-genCert/go.sum @@ -1,11 +1,3 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server-genCert/internal/cmd/root.go b/server-genCert/internal/cmd/root.go index e55c495a..6b6f30fb 100644 --- a/server-genCert/internal/cmd/root.go +++ b/server-genCert/internal/cmd/root.go @@ -39,9 +39,9 @@ var ( if !internal.GenerateCertificatePairs(serverFolder) { fmt.Println("Could not generate certificate pair.") os.Exit(internal.ErrCertCreate) - } else { - fmt.Println("Certificate pair generated successfully.") } + + fmt.Println("Certificate pair generated successfully.") }, } ) diff --git a/server-replay/go.sum b/server-replay/go.sum deleted file mode 100644 index 0095fd5b..00000000 --- a/server-replay/go.sum +++ /dev/null @@ -1,25 +0,0 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/r3labs/diff/v3 v3.0.2 h1:yVuxAY1V6MeM4+HNur92xkS39kB/N+cFi2hMkY06BbA= -github.com/r3labs/diff/v3 v3.0.2/go.mod h1:Cy542hv0BAEmhDYWtGxXRQ4kqRsVIcEjG9gChUlTmkw= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/README.md b/server/README.md index 705ded22..05e1fd5e 100644 --- a/server/README.md +++ b/server/README.md @@ -7,9 +7,9 @@ API requests. The server reimplements the minimum required API surface to allow #### Stable -- **Windows**: 10 (or equivalent, not Arm32). +- **Windows**: 7 (or equivalent). - **Linux**: kernel 3.2 (see [here](https://go.dev/wiki/Linux) for more details). -- **macOS**: Big Sur (v11). +- **macOS**: Monterey (v12). Admin rights or firewall permission to listen on port 443 (https) will likely be required depending on the operating system. @@ -21,7 +21,7 @@ system. - Solaris-based (Solaris and Illumos). - AIX. -Note: For the full list see [minimum requirements for Go](https://go.dev/wiki/MinimumRequirements) 1.24. +Note: For the full list see [minimum requirements for Go](https://go.dev/wiki/MinimumRequirements) 1.25. @@ -155,6 +155,11 @@ the [`responses`](resources/responses) base directory. flags, currently just disabling the beta tag for AotG. * [`known_blessings.json`](resources/responses/athens/playfab/public-production/2/known_blessings.json): Listing of all blessings, even blessings or levels not used. Used by the server to grant them all to the users. + * [`gauntlet.json`](resources/responses/athens/playfab/public-production/2/gauntlet.json): General + settings of the Challenge mode. + * [ + `gauntlet_mission_poools.json`](resources/responses/athens/playfab/public-production/2/gauntlet_mission_pools.json): + Listing of missions and their pools. ## Starting and configuring Online-like Battle Servers @@ -174,7 +179,7 @@ self-explanatory. ## Docker -See [Docker](../server-docker) for information. +See [Docker](../tools/server-docker) for information. ## Exit Codes diff --git a/server/go.mod b/server/go.mod index ced5a8ba..e5caaf1c 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,6 @@ module github.com/luskaner/ageLANServer/server -go 1.24.0 +go 1.25.5 require ( github.com/deckarep/golang-set/v2 v2.8.0 @@ -8,7 +8,6 @@ require ( github.com/gorilla/handlers v1.5.2 github.com/gorilla/schema v1.4.1 github.com/gorilla/websocket v1.5.3 - github.com/luskaner/ageLANServer/common v0.0.0-20251206212448-740b58cef4b4 github.com/miekg/dns v1.1.69 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 @@ -16,22 +15,25 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect - mvdan.cc/sh/v3 v3.12.0 // indirect + golang.org/x/tools v0.40.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/server/go.sum b/server/go.sum index f2d04f3a..fe34e6dc 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,81 +1,35 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= -github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/luskaner/ageLANServer/common v0.0.0-20251206212448-740b58cef4b4 h1:9h03yH40a3BDE2uqohbapVCSH0WJwmBOHZBqYi9Vqm0= -github.com/luskaner/ageLANServer/common v0.0.0-20251206212448-740b58cef4b4/go.mod h1:26bZchgUJvq0GsDu2I8h3qW9wNpWGa/0U6l7z4YEuv4= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= diff --git a/server/internal/Map.go b/server/internal/Map.go index a0a3c74c..084ec736 100644 --- a/server/internal/Map.go +++ b/server/internal/Map.go @@ -1,6 +1,7 @@ package internal import ( + "encoding/json" "iter" "maps" "slices" @@ -80,10 +81,11 @@ func (m *SafeMap[K, V]) CompareAndDelete(key K, compareFunc func(stored V) bool) return } -func (m *SafeMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { +func (m *SafeMap[K, V]) LoadOrStoreFn(key K, valueFn func() V) (actual V, loaded bool) { m.lock.Lock() defer m.lock.Unlock() if actual, loaded = m.wo[key]; !loaded { + value := valueFn() m.wo[key] = value m.updateReadOnly() actual = value @@ -102,10 +104,37 @@ func (m *SafeMap[K, V]) Values() iter.Seq[V] { } } +func (m *SafeMap[K, V]) Iter() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + roMap := m.ro.Load() + for key, value := range *roMap { + if !yield(key, value) { + break + } + } + } +} + func (m *SafeMap[K, V]) Len() int { return len(*m.ro.Load()) } +func (m *SafeMap[K, V]) MarshalJSON() (b []byte, err error) { + m.lock.Lock() + defer m.lock.Unlock() + return json.Marshal(m.wo) +} + +func (m *SafeMap[K, V]) UnmarshalJSON(b []byte) (err error) { + m.lock.Lock() + defer m.lock.Unlock() + if err = json.Unmarshal(b, &m.wo); err != nil { + return err + } + m.updateReadOnly() + return nil +} + type SafeSet[V comparable] struct { safeMap *SafeMap[V, any] } diff --git a/server/internal/cmd/root.go b/server/internal/cmd/root.go index 10e0aac4..0092823d 100644 --- a/server/internal/cmd/root.go +++ b/server/internal/cmd/root.go @@ -10,7 +10,6 @@ import ( "net/http" "net/netip" "os" - "path" "path/filepath" "runtime" "runtime/debug" @@ -23,8 +22,9 @@ import ( "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/cmd" "github.com/luskaner/ageLANServer/common/executor" + "github.com/luskaner/ageLANServer/common/fileLock" commonLogger "github.com/luskaner/ageLANServer/common/logger" - "github.com/luskaner/ageLANServer/common/pidLock" + "github.com/luskaner/ageLANServer/common/paths" "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/ip" "github.com/luskaner/ageLANServer/server/internal/logger" @@ -35,20 +35,22 @@ import ( "github.com/spf13/viper" ) -var configPaths = []string{path.Join("resources", "config"), "."} +var v = viper.New() + +var configPaths = []string{paths.ConfigsPath, "."} var id string var logRoot string var flatLog bool var deterministic bool +var cfgFile string var ( Version string - cfgFile string rootCmd = &cobra.Command{ Use: filepath.Base(os.Args[0]), Short: "server is a service for multiplayer features in AoE: DE, AoE 2: DE, AoE 3: DE and AoM: RT.", Run: func(_ *cobra.Command, _ []string) { - lock := &pidLock.Lock{} + lock := &fileLock.PidLock{} exitCode := common.ErrSuccess if err := lock.Lock(); err != nil { logger.Println("Failed to lock pid file. Kill process 'server' if it is running in your task manager.") @@ -56,14 +58,16 @@ var ( commonLogger.CloseFileLog() os.Exit(common.ErrPidLock) } + cfg := initConfig() commonLogger.Initialize(nil) if logRoot == "" { logRoot = commonLogger.LogRootDate("") } - if err := logger.OpenMainFileLog(logRoot); err != nil { + if err := logger.OpenMainFileLog(logRoot, cfg.Log); err != nil { logger.Printf("Failed to open main log file: %v", err) os.Exit(common.ErrFileLog) } + logger.PrintFile("config", v.ConfigFileUsed()) var seed uint64 if !deterministic { seed = uint64(time.Now().UnixNano()) @@ -93,10 +97,10 @@ var ( return } logger.Println("Server instance ID:", internal.Id) - if viper.GetBool("GeneratePlatformUserId") { + if cfg.GeneratePlatformUserId { logger.Println("Generating platform User ID, this should only be used as a last resort and the custom launcher should be properly configured instead.") } - gameSet := mapset.NewThreadUnsafeSet[string](viper.GetStringSlice("Games.Enabled")...) + gameSet := mapset.NewThreadUnsafeSet[string](cfg.Games.Enabled...) if gameSet.IsEmpty() { logger.Println("No games specified") exitCode = internal.ErrGames @@ -121,10 +125,10 @@ var ( exitCode = internal.ErrCertDirectory return } - announceEnabled := viper.GetBool("Announcement.Enabled") + announceEnabled := cfg.Announcement.Enabled multicastGroups := mapset.NewThreadUnsafeSet[netip.Addr]() - if announceEnabled && viper.GetBool("Announcement.Multicast") { - multicastIP, err := netip.ParseAddr(viper.GetString("Announcement.MulticastGroup")) + if announceEnabled && cfg.Announcement.Multicast { + multicastIP, err := netip.ParseAddr(cfg.Announcement.MulticastGroup) if err != nil || !multicastIP.Is4() || !multicastIP.IsMulticast() { logger.Println("Invalid multicast IP") if err != nil { @@ -135,25 +139,26 @@ var ( } multicastGroups.Add(multicastIP) } - announcePort := viper.GetInt("Announcement.Port") + announcePort := cfg.Announcement.Port internal.AnnounceMessageData = make(map[string]common.AnnounceMessageData002, gameSet.Cardinality()) + internal.GeneratePlatformUserId = cfg.GeneratePlatformUserId var servers []*http.Server internal.InitializeStopSignal() for gameId := range gameSet.Iter() { logger.Printf("Game %s:\n", gameId) - hosts := viper.GetStringSlice(fmt.Sprintf("Games.%s.Hosts", gameId)) + hosts := cfg.GetGameHosts(gameId) addrs := ip.ResolveHosts(mapset.NewThreadUnsafeSet[string](hosts...)) if addrs.IsEmpty() { logger.Println("\tFailed to resolve host (or it was an IPv6 address)") exitCode = internal.ErrResolveHost return } - if err = initializer.InitializeGame(gameId); err != nil { + if err = initializer.InitializeGame(gameId, cfg.GetGameBattleServers(gameId)); err != nil { logger.Printf("\tFailed to initialize game: %v\n", err) exitCode = internal.ErrGame return } - if battlesServers, ok := models.BattleServers[gameId]; ok && len(battlesServers) > 0 { + if battlesServers, ok := models.BattleServersStore[gameId]; ok && len(battlesServers) > 0 { logger.Println("\tBattle Servers:") for _, battleServer := range battlesServers { logger.Println("\t\t" + battleServer.String()) @@ -267,14 +272,12 @@ var ( defer cancel() for _, server := range servers { - wg.Add(1) - go func(s *http.Server) { - defer wg.Done() - if err := s.Shutdown(ctx); err != nil { - fmt.Printf("'Server' %s forced to shutdown: %v\n", s.Addr, err) + wg.Go(func() { + if err := server.Shutdown(ctx); err != nil { + fmt.Printf("'Server' %s forced to shutdown: %v\n", server.Addr, err) } - logger.Println("'Server'", s.Addr, "stopped") - }(server) + logger.Println("'Server'", server.Addr, "stopped") + }) } wg.Wait() }, @@ -282,7 +285,6 @@ var ( ) func Execute() error { - cobra.OnInitialize(initConfig) rootCmd.Version = Version rootCmd.Flags().StringVar(&cfgFile, "config", "", fmt.Sprintf(`config file (default config.toml in %s directories)`, strings.Join(configPaths, ", "))) rootCmd.Flags().StringP("announce", "a", "true", "Respond to discove 'server' in LAN. Disabling this will not allow launchers to discover it and will require specifying the host") @@ -296,42 +298,69 @@ func Execute() error { cmd.LogRootCommand(rootCmd.Flags(), &logRoot) rootCmd.Flags().BoolP("generatePlatformUserId", "g", false, "Generate the Platform User Id based on the user's IP.") rootCmd.Flags().StringVar(&id, "id", "", "Server instance ID to identify it.") + // Default Values + // General + v.SetDefault("Log", false) + v.SetDefault("GeneratePlatformUserId", false) + // Announcement + v.SetDefault("Announcement.Enabled", true) + v.SetDefault("Announcement.Multicast", true) + v.SetDefault("Announcement.MulticastGroup", common.AnnounceMulticastGroup) + v.SetDefault("Announcement.Port", common.AnnouncePort) + // Games + v.SetDefault("Games.Enabled", []string{}) + for game := range common.SupportedGames.Iter() { + v.SetDefault(fmt.Sprintf("Games.%s.Hosts", game), []string{netip.IPv4Unspecified().String()}) + } + // Bindings if err := viper.BindPFlag("Log", rootCmd.Flags().Lookup("log")); err != nil { return err } - if err := viper.BindPFlag("Announcement.Enabled", rootCmd.Flags().Lookup("announce")); err != nil { + if err := v.BindPFlag("Announcement.Enabled", rootCmd.Flags().Lookup("announce")); err != nil { return err } - if err := viper.BindPFlag("Announcement.Port", rootCmd.Flags().Lookup("announcePort")); err != nil { + if err := v.BindPFlag("Announcement.Port", rootCmd.Flags().Lookup("announcePort")); err != nil { return err } - if err := viper.BindPFlag("Announcement.Multicast", rootCmd.Flags().Lookup("announceMulticast")); err != nil { + if err := v.BindPFlag("Announcement.Multicast", rootCmd.Flags().Lookup("announceMulticast")); err != nil { return err } - if err := viper.BindPFlag("Announcement.MulticastGroup", rootCmd.Flags().Lookup("announceMulticastGroup")); err != nil { + if err := v.BindPFlag("Announcement.MulticastGroup", rootCmd.Flags().Lookup("announceMulticastGroup")); err != nil { return err } - if err := viper.BindPFlag("Games.Enabled", rootCmd.Flags().Lookup("games")); err != nil { + if err := v.BindPFlag("Games.Enabled", rootCmd.Flags().Lookup("games")); err != nil { return err } - if err := viper.BindPFlag("GeneratePlatformUserId", rootCmd.Flags().Lookup("generatePlatformUserId")); err != nil { + if err := v.BindPFlag("GeneratePlatformUserId", rootCmd.Flags().Lookup("generatePlatformUserId")); err != nil { return err } return rootCmd.Execute() } -func initConfig() { +func initConfig() *internal.Configuration { + for _, configPath := range configPaths { + v.AddConfigPath(configPath) + } + v.SetConfigType("toml") if cfgFile != "" { - viper.SetConfigFile(cfgFile) + v.SetConfigFile(cfgFile) + } else { + v.SetConfigName("config") + } + v.AutomaticEnv() + if err := v.ReadInConfig(); err == nil { + logger.Println("Using config file:", v.ConfigFileUsed()) } else { - for _, configPath := range configPaths { - viper.AddConfigPath(configPath) + var configFileNotFoundError viper.ConfigFileNotFoundError + if !errors.As(err, &configFileNotFoundError) { + logger.Println("Error parsing config file:", v.ConfigFileUsed()+":", err.Error()) + os.Exit(common.ErrConfigParse) } - viper.SetConfigType("toml") - viper.SetConfigName("config") } - viper.AutomaticEnv() - if err := viper.ReadInConfig(); err == nil { - logger.Println("Using config file:", viper.ConfigFileUsed()) + var c *internal.Configuration + if err := v.Unmarshal(&c); err != nil { + logger.Printf("unable to decode configuration: %v\n", err) + os.Exit(common.ErrConfigParse) } + return c } diff --git a/server/internal/config.go b/server/internal/config.go new file mode 100644 index 00000000..6ad93da8 --- /dev/null +++ b/server/internal/config.go @@ -0,0 +1,64 @@ +package internal + +import "github.com/luskaner/ageLANServer/common/battleServerConfig" + +type Announcement struct { + Enabled bool + Multicast bool + MulticastGroup string + Port int +} + +type BattleServer struct { + battleServerConfig.BaseConfig `mapstructure:",squash"` +} + +type Game struct { + Hosts []string + BattleServers []BattleServer +} + +type Games struct { + Enabled []string + Age1 Game + Age2 Game + Age3 Game + Athens Game +} + +type Configuration struct { + Log bool + GeneratePlatformUserId bool + Announcement Announcement + Games Games +} + +func (cfg *Configuration) GetGameHosts(gameId string) []string { + switch gameId { + case "age1": + return cfg.Games.Age1.Hosts + case "age2": + return cfg.Games.Age2.Hosts + case "age3": + return cfg.Games.Age3.Hosts + case "athens": + return cfg.Games.Athens.Hosts + default: + return nil + } +} + +func (cfg *Configuration) GetGameBattleServers(gameId string) []BattleServer { + switch gameId { + case "age1": + return cfg.Games.Age1.BattleServers + case "age2": + return cfg.Games.Age2.BattleServers + case "age3": + return cfg.Games.Age3.BattleServers + case "athens": + return cfg.Games.Athens.BattleServers + default: + return nil + } +} diff --git a/server/internal/http.go b/server/internal/http.go index a6430a07..b221c8f5 100644 --- a/server/internal/http.go +++ b/server/internal/http.go @@ -3,12 +3,22 @@ package internal import ( "encoding/json" "errors" + "io" "net/http" + "strings" "github.com/gorilla/schema" ) -type A []any +type Json[T any] struct { + Data T +} + +func (j *Json[T]) UnmarshalText(text []byte) (err error) { + return json.Unmarshal(text, &j.Data) +} + +type A = []any type H map[string]any var decoder = schema.NewDecoder() @@ -27,7 +37,7 @@ func RawJSON(w *http.ResponseWriter, data []byte) { _, _ = (*w).Write(data) } -func decode(dst interface{}, src map[string][]string) error { +func decode(dst any, src map[string][]string) error { err := decoder.Decode(dst, src) if err == nil { return nil @@ -53,6 +63,11 @@ func Bind[D any](r *http.Request, data *D) error { var err error if r.Method == http.MethodGet { err = decode(data, r.URL.Query()) + } else if strings.Contains(r.Header.Get("Content-Type"), "application/json") { + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(r.Body) + err = json.NewDecoder(r.Body).Decode(data) } else { err = r.ParseForm() if err != nil { diff --git a/server/internal/ip/announce.go b/server/internal/ip/announce.go index 958fede2..244c1b78 100644 --- a/server/internal/ip/announce.go +++ b/server/internal/ip/announce.go @@ -39,6 +39,7 @@ func QueryConnections(ipAddr netip.Addr, multicastGroups mapset.Set[netip.Addr], if conn, err = net.ListenUDP("udp4", addr); err == nil { var pckConn *ipv4.PacketConn if !multicastGroups.IsEmpty() { + //goland:noinspection GoResourceLeak pckConn = ipv4.NewPacketConn(conn) } for multicastGroup := range multicastGroups.Iter() { @@ -47,7 +48,8 @@ func QueryConnections(ipAddr netip.Addr, multicastGroups mapset.Set[netip.Addr], Port: port, } for _, multicastIf := range multicastIfs { - if err = pckConn.JoinGroup(multicastIf, multicastAddr); err != nil { + if //goland:noinspection ALL + err = pckConn.JoinGroup(multicastIf, multicastAddr); err != nil { continue } } diff --git a/server/internal/logger/log.go b/server/internal/logger/log.go index 315c9e02..48b60842 100644 --- a/server/internal/logger/log.go +++ b/server/internal/logger/log.go @@ -2,10 +2,10 @@ package logger import ( "fmt" + "os" "time" - "github.com/luskaner/ageLANServer/common/logger" - "github.com/spf13/viper" + commonLogger "github.com/luskaner/ageLANServer/common/logger" ) var StartTime time.Time @@ -14,8 +14,8 @@ func init() { StartTime = time.Now().UTC() } -func OpenMainFileLog(root string) error { - if viper.GetBool("Log") { +func OpenMainFileLog(root string, logEnabled bool) error { + if logEnabled { err := commonLogger.NewOwnFileLogger("server", root, "", true) if err != nil { return err @@ -24,6 +24,13 @@ func OpenMainFileLog(root string) error { return nil } +func PrintFile(name string, path string) { + if commonLogger.FileLogger != nil && path != "" { + data, _ := os.ReadFile(path) + commonLogger.PrefixPrintln(name, string(data)) + } +} + func Printf(format string, a ...any) { commonLogger.PrefixPrintf("main", format, a...) fmt.Printf(format, a...) diff --git a/server/internal/models/advertisement.go b/server/internal/models/advertisement.go index 142098e1..5242e723 100644 --- a/server/internal/models/advertisement.go +++ b/server/internal/models/advertisement.go @@ -32,6 +32,42 @@ type Tags struct { text map[string]string } +type Advertisement interface { + GetMetadata() string + UnsafeGetModDllChecksum() int32 + UnsafeGetModDllFile() string + UnsafeGetPasswordValue() string + UnsafeGetStartTime() int64 + UnsafeGetState() int8 + UnsafeGetDescription() string + GetRelayRegion() string + UnsafeGetVisible() bool + UnsafeGetJoinable() bool + UnsafeGetAppBinaryChecksum() int32 + UnsafeGetDataChecksum() int32 + UnsafeGetMatchType() uint8 + UnsafeGetModName() string + UnsafeGetModVersion() string + UnsafeGetVersionFlags() uint32 + UnsafeGetPlatformSessionId() uint64 + UnsafeGetObserversDelay() uint32 + UnsafeGetObserversEnabled() bool + UnsafeUpdateState(state int8) + UnsafeUpdatePlatformSessionId(sessionId uint64) + UnsafeUpdateTags(integer map[string]int32, text map[string]string) + UnsafeMatchesTags(integer map[string]int32, text map[string]string) bool + UnsafeEncode(gameId string, battleServers BattleServers) i.A + UnsafeUpdate(advFrom *shared.AdvertisementUpdateRequest) + GetId() int32 + GetIp() string + GetHostId() int32 + GetPeers() *i.SafeOrderedMap[int32, Peer] + MakeMessage(broadcast bool, content string, typeId uint8, sender User, receivers []User) Message + StartObserving(userId int32) + StopObserving(userId int32) + EncodePeers() []i.A +} + type MainAdvertisement struct { id int32 ip string @@ -61,7 +97,7 @@ type MainAdvertisement struct { platformSessionId uint64 state int8 startTime int64 - peers *i.SafeOrderedMap[int32, *MainPeer] + peers *i.SafeOrderedMap[int32, Peer] metadata string tags Tags } @@ -75,15 +111,29 @@ func containsFilter[M ~map[K]V, K, V comparable](filter M, tags M) bool { return true } +type Advertisements interface { + Initialize(users Users, battleServers BattleServers) + Store(advFrom *shared.AdvertisementHostRequest, generateMetadata bool, alternateScid bool) Advertisement + WithReadLock(id int32, action func()) + WithWriteLock(id int32, action func()) + GetAdvertisement(id int32) (Advertisement, bool) + UnsafeNewPeer(advertisementId int32, advertisementIp string, userId int32, userStatId int32, race int32, team int32) Peer + UnsafeRemovePeer(advertisementId int32, userId int32) bool + UnsafeDelete(adv Advertisement) + UnsafeFirstAdvertisement(matches func(adv Advertisement) bool) Advertisement + LockedFindAdvertisementsEncoded(gameId string, length int, offset int, preMatchesLocking bool, matches func(adv Advertisement) bool) i.A + GetUserAdvertisement(userId int32) Advertisement +} + type MainAdvertisements struct { - store *i.SafeOrderedMap[int32, *MainAdvertisement] + store *i.SafeOrderedMap[int32, Advertisement] locks *i.KeyRWMutex[int32] - users *MainUsers - battleServers *MainBattleServers + users Users + battleServers BattleServers } -func (advs *MainAdvertisements) Initialize(users *MainUsers, battleServers *MainBattleServers) { - advs.store = i.NewSafeOrderedMap[int32, *MainAdvertisement]() +func (advs *MainAdvertisements) Initialize(users Users, battleServers BattleServers) { + advs.store = i.NewSafeOrderedMap[int32, Advertisement]() advs.locks = i.NewKeyRWMutex[int32]() advs.users = users advs.battleServers = battleServers @@ -194,11 +244,11 @@ func (adv *MainAdvertisement) UnsafeGetObserversEnabled() bool { return adv.observers.enabled } -func (adv *MainAdvertisement) GetPeers() *i.SafeOrderedMap[int32, *MainPeer] { +func (adv *MainAdvertisement) GetPeers() *i.SafeOrderedMap[int32, Peer] { return adv.peers } -func (advs *MainAdvertisements) Store(advFrom *shared.AdvertisementHostRequest, generateMetadata bool, alternateScid bool) *MainAdvertisement { +func (advs *MainAdvertisements) Store(advFrom *shared.AdvertisementHostRequest, generateMetadata bool, alternateScid bool) Advertisement { adv := &MainAdvertisement{} i.WithRng(func(rand *i.RandReader) { adv.ip = fmt.Sprintf("/10.0.11.%d", rand.IntN(254)+1) @@ -226,8 +276,8 @@ func (advs *MainAdvertisements) Store(advFrom *shared.AdvertisementHostRequest, adv.statGroup = advFrom.StatGroup adv.tags.text = make(map[string]string) adv.tags.integer = make(map[string]int32) - adv.peers = i.NewSafeOrderedMap[int32, *MainPeer]() - advs.UpdateUnsafe(adv, &shared.AdvertisementUpdateRequest{ + adv.peers = i.NewSafeOrderedMap[int32, Peer]() + adv.UnsafeUpdate(&shared.AdvertisementUpdateRequest{ AppBinaryChecksum: advFrom.AppBinaryChecksum, DataChecksum: advFrom.DataChecksum, ModDllChecksum: advFrom.ModDllChecksum, @@ -252,19 +302,19 @@ func (advs *MainAdvertisements) Store(advFrom *shared.AdvertisementHostRequest, State: advFrom.State, }) exists := true - var storedAdv *MainAdvertisement + var storedAdv Advertisement for exists { i.WithRng(func(rand *i.RandReader) { adv.id = rand.Int32() }) - exists, storedAdv = advs.store.Store(adv.id, adv, func(_ *MainAdvertisement) bool { + exists, storedAdv = advs.store.Store(adv.id, adv, func(_ Advertisement) bool { return false }) } return storedAdv } -func (adv *MainAdvertisement) MakeMessage(broadcast bool, content string, typeId uint8, sender *MainUser, receivers []*MainUser) *MainMessage { +func (adv *MainAdvertisement) MakeMessage(broadcast bool, content string, typeId uint8, sender User, receivers []User) Message { return &MainMessage{ advertisementId: adv.GetId(), time: time.Now().UTC().Unix(), @@ -288,8 +338,8 @@ func (advs *MainAdvertisements) WithWriteLock(id int32, action func()) { action() } -// UpdateUnsafe is safe only if adv has not been stored yet -func (advs *MainAdvertisements) UpdateUnsafe(adv *MainAdvertisement, advFrom *shared.AdvertisementUpdateRequest) { +// UnsafeUpdate is safe only if adv has not been stored yet +func (adv *MainAdvertisement) UnsafeUpdate(advFrom *shared.AdvertisementUpdateRequest) { adv.automatchPollId = advFrom.AutomatchPollId adv.appBinaryChecksum = advFrom.AppBinaryChecksum adv.mapName = advFrom.MapName @@ -314,18 +364,18 @@ func (advs *MainAdvertisements) UpdateUnsafe(adv *MainAdvertisement, advFrom *sh adv.UnsafeUpdateState(advFrom.State) } -func (advs *MainAdvertisements) GetAdvertisement(id int32) (*MainAdvertisement, bool) { +func (advs *MainAdvertisements) GetAdvertisement(id int32) (Advertisement, bool) { return advs.store.Load(id) } // UnsafeNewPeer requires advertisement write lock -func (advs *MainAdvertisements) UnsafeNewPeer(advertisementId int32, advertisementIp string, userId int32, userStatId int32, race int32, team int32) *MainPeer { +func (advs *MainAdvertisements) UnsafeNewPeer(advertisementId int32, advertisementIp string, userId int32, userStatId int32, race int32, team int32) Peer { adv, exists := advs.GetAdvertisement(advertisementId) if !exists { return nil } peer := NewPeer(advertisementId, advertisementIp, userId, userStatId, race, team) - _, storedPeer := adv.peers.Store(peer.userId, peer, func(_ *MainPeer) bool { + _, storedPeer := adv.GetPeers().Store(peer.GetUserId(), peer, func(_ Peer) bool { return false }) return storedPeer @@ -337,18 +387,18 @@ func (advs *MainAdvertisements) UnsafeRemovePeer(advertisementId int32, userId i if !exists { return false } - if !adv.peers.Delete(userId) { + if !adv.GetPeers().Delete(userId) { return false } - if adv.hostId == userId { + if adv.GetHostId() == userId { advs.UnsafeDelete(adv) } return true } // UnsafeDelete requires advertisement write lock -func (advs *MainAdvertisements) UnsafeDelete(adv *MainAdvertisement) { - advs.store.Delete(adv.id) +func (advs *MainAdvertisements) UnsafeDelete(adv Advertisement) { + advs.store.Delete(adv.GetId()) } // UnsafeUpdateState is only safe if advertisement has not been added yet @@ -399,7 +449,7 @@ func (adv *MainAdvertisement) UnsafeMatchesTags(integer map[string]int32, text m } // UnsafeEncode requires advertisement read lock -func (adv *MainAdvertisement) UnsafeEncode(gameId string, battleServers *MainBattleServers) i.A { +func (adv *MainAdvertisement) UnsafeEncode(gameId string, battleServers BattleServers) i.A { var visible uint8 if adv.visible { visible = 1 @@ -447,7 +497,7 @@ func (adv *MainAdvertisement) UnsafeEncode(gameId string, battleServers *MainBat response = append(response, adv.description) } lan := 1 - var battleServer *MainBattleServer + var battleServer BattleServer var battleServerExists bool if battleServer, battleServerExists = battleServers.Get(adv.relayRegion); battleServerExists { lan = 0 @@ -480,7 +530,7 @@ func (adv *MainAdvertisement) UnsafeEncode(gameId string, battleServers *MainBat } // UnsafeFirstAdvertisement requires advertisement read lock unless only safe advertisement properties are checked -func (advs *MainAdvertisements) UnsafeFirstAdvertisement(matches func(adv *MainAdvertisement) bool) *MainAdvertisement { +func (advs *MainAdvertisements) UnsafeFirstAdvertisement(matches func(adv Advertisement) bool) Advertisement { _, iter := advs.store.Values() for adv := range iter { if matches(adv) { @@ -490,7 +540,7 @@ func (advs *MainAdvertisements) UnsafeFirstAdvertisement(matches func(adv *MainA return nil } -func (advs *MainAdvertisements) LockedFindAdvertisementsEncoded(gameId string, length int, offset int, preMatchesLocking bool, matches func(adv *MainAdvertisement) bool) i.A { +func (advs *MainAdvertisements) LockedFindAdvertisementsEncoded(gameId string, length int, offset int, preMatchesLocking bool, matches func(adv Advertisement) bool) i.A { var res i.A _, iter := advs.store.Values() for adv := range iter { @@ -522,9 +572,9 @@ func (advs *MainAdvertisements) LockedFindAdvertisementsEncoded(gameId string, l return res[offset:end] } -func (advs *MainAdvertisements) GetUserAdvertisement(userId int32) *MainAdvertisement { - return advs.UnsafeFirstAdvertisement(func(adv *MainAdvertisement) bool { - _, peerIter := adv.peers.Keys() +func (advs *MainAdvertisements) GetUserAdvertisement(userId int32) Advertisement { + return advs.UnsafeFirstAdvertisement(func(adv Advertisement) bool { + _, peerIter := adv.GetPeers().Keys() for usId := range peerIter { if usId == userId { return true diff --git a/server/internal/models/athens/game.go b/server/internal/models/athens/game.go index 49c0c42e..6890628b 100644 --- a/server/internal/models/athens/game.go +++ b/server/internal/models/athens/game.go @@ -5,28 +5,50 @@ import ( "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" - "github.com/luskaner/ageLANServer/server/internal/models/athens/game/communityEvent" - "github.com/luskaner/ageLANServer/server/internal/models/athens/playfab" + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/game/communityEvent" + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/playfab" + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/precomputed" + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" commonPlayfab "github.com/luskaner/ageLANServer/server/internal/models/playfab" ) type Game struct { - models.MainGame - Blessings []playfab.Blessings - CatalogItems map[string]commonPlayfab.CatalogItem + models.Game + AllowedBlessings map[int][]string + GauntletPoolIndexByDifficulty map[string][]int + Gauntlet playfab.Gauntlet + GauntletMissionPools playfab.GauntletMissionPools + CatalogItems map[string]commonPlayfab.CatalogItem // All users have the same fixed items - InventoryItems []commonPlayfab.InventoryItem + InventoryItems []commonPlayfab.InventoryItem + PlayfabSessions commonPlayfab.MainSessions } func CreateGame() models.Game { mainGame := models.CreateMainGame( common.GameAoM, + nil, + nil, + nil, + &user.Users{}, + nil, + nil, + nil, mapset.NewThreadUnsafeSet[string]("itemBundleItems.json", "itemDefinitions.json"), true, "true", ) - g := &Game{MainGame: *mainGame, Blessings: playfab.ReadBlessings()} - g.CatalogItems, g.InventoryItems = playfab.Items(g.Blessings) + g := &Game{ + Game: mainGame, + } + blessings := playfab.ReadBlessings() + g.CatalogItems, g.InventoryItems = playfab.Items(blessings) + g.Gauntlet = playfab.ReadGauntlet() + g.GauntletMissionPools = playfab.ReadGauntletMissionPools() + g.AllowedBlessings = precomputed.AllowedGauntletBlessings(g.Gauntlet, blessings) + gauntletPoolNamesToIndex := precomputed.PoolNamesToIndex(g.GauntletMissionPools) + g.GauntletPoolIndexByDifficulty = precomputed.PoolsIndexByDifficulty(g.Gauntlet, gauntletPoolNamesToIndex) + g.PlayfabSessions.Initialize() communityEvent.Initialize() return g } diff --git a/server/internal/models/athens/playfab/cloudScriptFunction/awardMissionRewards.go b/server/internal/models/athens/playfab/cloudScriptFunction/awardMissionRewards.go deleted file mode 100644 index f85ebd5c..00000000 --- a/server/internal/models/athens/playfab/cloudScriptFunction/awardMissionRewards.go +++ /dev/null @@ -1,29 +0,0 @@ -package cloudScriptFunction - -type AwardMissionRewardsParameters struct { - CdnPath string - Difficulty string - MissionId string - Won bool -} - -type AwardMissionRewardsResultItemsAdded struct { - Amount int `json:"amount"` - ItemFriendlyId string `json:"itemFriendlyId"` -} - -type AwardMissionRewardsResult struct { - ItemsAdded []AwardMissionRewardsResultItemsAdded `json:"itemsAdded"` -} - -type AwardMissionRewardsFunction struct{} - -func (a *AwardMissionRewardsFunction) Run(_ AwardMissionRewardsParameters) AwardMissionRewardsResult { - return AwardMissionRewardsResult{ - ItemsAdded: []AwardMissionRewardsResultItemsAdded{}, - } -} - -func (a *AwardMissionRewardsFunction) Name() string { - return "AwardMissionRewards" -} diff --git a/server/internal/models/athens/playfab/cloudScriptFunction/cloudScriptFunction.go b/server/internal/models/athens/playfab/cloudScriptFunction/cloudScriptFunction.go deleted file mode 100644 index 6d328c4b..00000000 --- a/server/internal/models/athens/playfab/cloudScriptFunction/cloudScriptFunction.go +++ /dev/null @@ -1,21 +0,0 @@ -package cloudScriptFunction - -import ( - "github.com/luskaner/ageLANServer/server/internal/models/playfab" -) - -var Store map[string]SpecificCloudScriptFunction -var fns = []SpecificCloudScriptFunction{ - &AwardMissionRewardsFunction{}, -} - -type SpecificCloudScriptFunction interface { - playfab.CloudScriptFunction[AwardMissionRewardsParameters, AwardMissionRewardsResult] -} - -func init() { - Store = make(map[string]SpecificCloudScriptFunction, len(fns)) - for _, fn := range fns { - Store[fn.Name()] = fn - } -} diff --git a/server/internal/models/athens/playfab/items.go b/server/internal/models/athens/playfab/items.go deleted file mode 100644 index 70ce7d0c..00000000 --- a/server/internal/models/athens/playfab/items.go +++ /dev/null @@ -1,55 +0,0 @@ -package playfab - -import ( - "fmt" - "strconv" - "time" - - "github.com/luskaner/ageLANServer/server/internal/models/playfab" -) - -func Items(blessings []Blessings) (catalogItems map[string]playfab.CatalogItem, inventoryItems []playfab.InventoryItem) { - catalogItems = make(map[string]playfab.CatalogItem) - for _, b := range blessings { - prefix := fmt.Sprintf("Item_Season0_%s_", b.EffectName) - for _, r := range b.KnownRarities { - if r > -1 { - name := prefix + strconv.Itoa(r) - inventoryItems = append( - inventoryItems, - playfab.InventoryItem{ - Id: name, - StackId: "default", - Amount: 1, - Type: "catalogItem", - }, - ) - dateFormatted := time.Date(2024, 5, 2, 3, 34, 0, 0, time.UTC).Format(playfab.Iso8601Layout) - catalogItems[name] = - playfab.CatalogItem{ - Id: name, - Type: "catalogItem", - AlternateIds: []playfab.CatalogItemAlternativeId{ - { - "FriendlyId", - name, - }, - }, - FriendlyId: name, - Title: playfab.CatalogItemTitle{NEUTRAL: name}, - CreatorEntity: playfab.CatalogItemCreatorEntity{Id: "C15F9", Type: "title", TypeString: "title"}, - Platforms: []any{}, - Tags: []any{}, - CreationDate: dateFormatted, - LastModifiedDate: dateFormatted, - StartDate: dateFormatted, - Contents: []any{}, - Images: []any{}, - ItemReferences: []any{}, - DeepLinks: []any{}, - } - } - } - } - return -} diff --git a/server/internal/models/athens/game/communityEvent/communityEvent.go b/server/internal/models/athens/routes/game/communityEvent/communityEvent.go similarity index 100% rename from server/internal/models/athens/game/communityEvent/communityEvent.go rename to server/internal/models/athens/routes/game/communityEvent/communityEvent.go diff --git a/server/internal/models/athens/game/communityEvent/communityEvents.go b/server/internal/models/athens/routes/game/communityEvent/communityEvents.go similarity index 100% rename from server/internal/models/athens/game/communityEvent/communityEvents.go rename to server/internal/models/athens/routes/game/communityEvent/communityEvents.go diff --git a/server/internal/models/athens/playfab/blessings.go b/server/internal/models/athens/routes/playfab/blessings.go similarity index 100% rename from server/internal/models/athens/playfab/blessings.go rename to server/internal/models/athens/routes/playfab/blessings.go diff --git a/server/internal/models/athens/routes/playfab/cloudScriptFunction/awardMissionRewards.go b/server/internal/models/athens/routes/playfab/cloudScriptFunction/awardMissionRewards.go new file mode 100644 index 00000000..d7cddc5f --- /dev/null +++ b/server/internal/models/athens/routes/playfab/cloudScriptFunction/awardMissionRewards.go @@ -0,0 +1,89 @@ +package cloudScriptFunction + +import ( + "fmt" + "strconv" + "strings" + + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" + "github.com/luskaner/ageLANServer/server/internal/models/playfab" +) + +type AwardMissionRewardsParameters struct { + CdnPath string + Difficulty string + MissionId string + Won bool +} + +type AwardMissionRewardsResultItemsAdded struct { + Amount int `json:"amount"` + ItemFriendlyId string `json:"itemFriendlyId"` +} + +type AwardMissionRewardsResult struct { + ItemsAdded []AwardMissionRewardsResultItemsAdded `json:"itemsAdded"` +} + +type AwardMissionRewardsFunction struct { + *playfab.CloudScriptFunctionBase[AwardMissionRewardsParameters, AwardMissionRewardsResult] +} + +func (a *AwardMissionRewardsFunction) RunTyped(_ models.Game, u models.User, parameters *AwardMissionRewardsParameters) *AwardMissionRewardsResult { + result := &AwardMissionRewardsResult{ + ItemsAdded: []AwardMissionRewardsResultItemsAdded{}, + } + if elements := strings.Split(parameters.MissionId, "_"); elements[1] == "Gauntlet" { + actualMissionId := strings.Join(elements[2:], "_") + athensUser := u.(*user.User) + _ = athensUser.PlayfabData.WithReadWrite(func(data *user.Data) error { + challenge := data.Challenge + progress := challenge.Progress + if progress == nil { + return fmt.Errorf("no challenge progress found") + } + progressValue := progress.Value + if progressValue.MissionBeingPlayedRightNow != parameters.MissionId { + return fmt.Errorf("challenge progress not correct") + } + if parameters.Won { + progressValue.CompletedMissions = append(progressValue.CompletedMissions, parameters.MissionId) + for _, mission := range (*challenge.Labyrinth.Value).Missions { + if mission.Id != actualMissionId { + continue + } + for _, reward := range mission.Rewards { + elements = strings.Split(reward.ItemId, "_") + rarity, _ := strconv.Atoi(elements[3]) + progressValue.Inventory = append(progressValue.Inventory, user.ProgressInventory{ + SeasonId: "Gauntlet", + Item: elements[2], + Rarity: rarity, + }) + result.ItemsAdded = append(result.ItemsAdded, AwardMissionRewardsResultItemsAdded{ + Amount: reward.Amount, + ItemFriendlyId: reward.ItemId, + }) + } + } + progressValue.MissionBeingPlayedRightNow = "" + progress.UpdateLastUpdated() + data.DataVersion++ + return nil + } + return fmt.Errorf("no rewards awarded for lost mission") + }) + } + return result +} + +func (a *AwardMissionRewardsFunction) Name() string { + return "AwardMissionRewards" +} + +func NewAwardMissionRewardsFunction() *AwardMissionRewardsFunction { + f := &AwardMissionRewardsFunction{} + f.CloudScriptFunctionBase = playfab.NewCloudScriptFunctionBase[AwardMissionRewardsParameters, AwardMissionRewardsResult](f) + return f +} diff --git a/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/buildGauntletLabyrinth.go b/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/buildGauntletLabyrinth.go new file mode 100644 index 00000000..cdb00f2f --- /dev/null +++ b/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/buildGauntletLabyrinth.go @@ -0,0 +1,77 @@ +package buildGauntletLabyrinth + +import ( + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/athens" + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" + "github.com/luskaner/ageLANServer/server/internal/models/playfab" + userData "github.com/luskaner/ageLANServer/server/internal/models/playfab/data" +) + +type Result struct { + Labyrinth *user.Labyrinth + Progress *user.Progress +} + +type Parameters struct { + CdnPath string + GauntletDifficulty string +} + +type Function struct { + *playfab.CloudScriptFunctionBase[Parameters, Result] +} + +func (b *Function) RunTyped(game models.Game, u models.User, parameters *Parameters) *Result { + athensGame := game.(*athens.Game) + athensUser := u.(*user.User) + nodes := GenerateNumberOfNodes() + nodeRows := GenerateNodeRows(nodes) + blessings := RandomizedBlessings(athensGame.AllowedBlessings) + poolIndexes := athensGame.GauntletPoolIndexByDifficulty[parameters.GauntletDifficulty] + missionColumns := GenerateMissions(nodeRows, poolIndexes, athensGame.GauntletMissionPools, blessings) + var missions []user.ChallengeMission + for _, missionColumn := range missionColumns { + for _, mission := range missionColumn { + missions = append(missions, *mission) + } + } + var result *Result + _ = athensUser.PlayfabData.WithReadWrite(func(data *user.Data) error { + var id int + if labyrinth := data.Challenge.Labyrinth; labyrinth != nil { + id = (*labyrinth.Value).Id + 1 + } else { + id = 1 + } + data.Challenge = user.Challenge{ + Labyrinth: userData.NewPrivateBaseValue(user.Labyrinth{ + Id: id, + Difficulty: parameters.GauntletDifficulty, + Missions: missions, + }), + Progress: userData.NewPrivateBaseValue(user.Progress{ + Lives: 3, + CompletedMissions: []string{}, + Inventory: []user.ProgressInventory{}, + }), + } + data.DataVersion++ + result = &Result{ + Labyrinth: data.Challenge.Labyrinth.Value, + Progress: data.Challenge.Progress.Value, + } + return nil + }) + return result +} + +func (b *Function) Name() string { + return "BuildGauntletLabyrinth" +} + +func NewFunction() *Function { + f := &Function{} + f.CloudScriptFunctionBase = playfab.NewCloudScriptFunctionBase[Parameters, Result](f) + return f +} diff --git a/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/generator.go b/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/generator.go new file mode 100644 index 00000000..c820c2e3 --- /dev/null +++ b/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/generator.go @@ -0,0 +1,289 @@ +package buildGauntletLabyrinth + +import ( + "fmt" + "math" + "math/rand" + "slices" + + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/playfab" + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" +) + +import ( + mapset "github.com/deckarep/golang-set/v2" +) + +const columns = 10 +const rows = 6 + +var window = []int{-1, 0, +1} +var windowLen int + +func init() { + windowLen = len(window) +} + +type Range struct { + Min int + Max int +} + +func (r Range) RandomValue() int { + return rand.Intn(r.Max-r.Min+1) + r.Min +} + +var nodesPerColumn = []Range{ + // Initial column + {Min: 2, Max: 3}, + // Middle columns + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + {Min: 2, Max: 6}, + // Boss column + {Min: 1, Max: 1}, +} + +// blessingLevelPerColumn indicates the blessing level for each columm +var blessingLevelPerColumn = []int{0, 0, 1, 1, 2, 2, 3, 3, 4, 5} + +func GenerateNumberOfNodes() []int { + nodes := make([]int, columns) + nodes[0] = nodesPerColumn[0].RandomValue() + for col, rang := range nodesPerColumn[1 : len(nodesPerColumn)-1] { + index := col + 1 + previousNodes := nodes[index-1] + maxNodes := math.Min(rows, float64(previousNodes*2+1)) + minNodes := (previousNodes + windowLen - 1) / windowLen + finalRng := Range{ + Min: int(math.Max(float64(rang.Min), float64(minNodes))), + Max: int(math.Min(float64(rang.Max), maxNodes)), + } + nodes[index] = finalRng.RandomValue() + } + nodes[columns-1] = nodesPerColumn[columns-1].RandomValue() + return nodes +} + +func connectablePositions(position int) (positions []int) { + for _, pos := range window { + computedPos := position + pos + if computedPos > -1 && computedPos < rows { + positions = append(positions, computedPos) + } + } + return +} + +func computePositions(previousNodes []int, numberOfNodes int) []int { + nodeToPositions := make(map[int]mapset.Set[int], rows) + positionToNodes := make(map[int]mapset.Set[int], rows) + for i := 0; i < rows; i++ { + positionToNodes[i] = mapset.NewThreadUnsafeSet[int]() + } + for _, node := range previousNodes { + positions := connectablePositions(node) + nodeToPositions[node] = mapset.NewThreadUnsafeSet[int](positions...) + for _, pos := range positions { + positionToNodes[pos].Add(node) + } + } + finalPositions := mapset.NewThreadUnsafeSet[int]() + for _, positions := range nodeToPositions { + best := -1 + bestCard := -1 + for _, pos := range positions.ToSlice() { + card := positionToNodes[pos].Cardinality() + if card > bestCard { + best = pos + bestCard = card + } + } + if best != -1 { + finalPositions.Add(best) + } + } + for i := 1; i <= windowLen && finalPositions.Cardinality() < numberOfNodes; i++ { + acumPositions := mapset.NewThreadUnsafeSet[int]() + for _, positions := range nodeToPositions { + if positions.Cardinality() != i { + continue + } + acumPositions.Append(positions.ToSlice()...) + } + for j := windowLen; j > 0 && finalPositions.Cardinality() < numberOfNodes; j-- { + var currentPositions []int + for position, nodes := range positionToNodes { + if nodes.Cardinality() != j || !acumPositions.Contains(position) { + continue + } + if !finalPositions.Contains(position) { + currentPositions = append(currentPositions, position) + } + } + if len(currentPositions) > 1 { + shuffle(currentPositions) + } + positionsToTake := min(numberOfNodes-finalPositions.Cardinality(), len(currentPositions)) + if positionsToTake > 0 { + finalPositions.Append(currentPositions[:positionsToTake]...) + } + } + } + return finalPositions.ToSlice() +} + +func shuffle[T any](slice []T) { + rand.Shuffle(len(slice), func(i, j int) { + slice[i], slice[j] = slice[j], slice[i] + }) +} + +func GenerateNodeRows(numberOfNodes []int) [][]int { + nodes := make([][]int, columns) + // The first positions can be completely random + positions := make([]int, rows) + for i := 0; i < rows; i++ { + positions[i] = i + } + shuffle(positions) + nodes[0] = positions[:numberOfNodes[0]] + for col, numberOfColumnNodes := range numberOfNodes[:len(numberOfNodes)-1] { + finalCol := col + 1 + nodes[finalCol] = computePositions(nodes[finalCol-1], numberOfColumnNodes) + } + // Last node as it is a boss node + nodes[len(numberOfNodes)-1] = []int{0} + return nodes +} + +func RandomizedBlessings(blessings map[int][]string) (randomBlessings map[int][]string) { + randomBlessings = make(map[int][]string, len(blessings)) + for k, v := range blessings { + newSlice := make([]string, len(v)) + copy(newSlice, v) + shuffle(newSlice) + randomBlessings[k] = newSlice + } + return randomBlessings +} + +func GenerateMissions(nodeRows [][]int, poolsIndexes []int, missionsPools playfab.GauntletMissionPools, blessings map[int][]string) (missionColumns [][]*user.ChallengeMission) { + missionColumns = make([][]*user.ChallengeMission, len(nodeRows)) + blessingIndexes := make(map[int]int) + nodePosToIndex := make([]map[int]int, len(nodeRows)) + for col, nodes := range nodeRows { + nodePosToIndex[col] = make(map[int]int, len(nodes)) + missionsColumn := make([]*user.ChallengeMission, len(nodes)) + pool := missionsPools[poolsIndexes[col]] + allMissions := pool.Missions + indexesChoosen := rand.Perm(len(allMissions))[:len(nodes)] + blessingLevel := blessingLevelPerColumn[col] + allBlessings := blessings[blessingLevel] + for i, node := range nodes { + var X, Y int + var visualization string + predecessors := make([]string, 0) + if col == columns-1 { + for _, predecessor := range missionColumns[col-1] { + predecessors = append(predecessors, predecessor.Id) + } + X = 1600 + visualization = "UberBoss" + } else { + if col > 0 { + for _, previousColumnMission := range missionColumns[col-1] { + if !slices.Contains(connectablePositions(previousColumnMission.RowIndex), node) { + continue + } + predecessors = append(predecessors, previousColumnMission.Id) + } + } + Y = -350 + 140*node + X = 80 + 160*col + visualization = "Regular" + } + unusedBlessing := allBlessings[blessingIndexes[blessingLevel]] + mission := allMissions[indexesChoosen[i]] + missionsColumn[i] = &user.ChallengeMission{ + RowIndex: node, + Id: fmt.Sprintf("%d_%d_%s/%s", col, node, pool.Name, mission.Id), + Predecessors: predecessors, + PositionX: X, + PositionY: Y, + Visualization: visualization, + Size: mission.Size, + VictoryCondition: "Standard", + GameType: "Standard", + MapVisibility: mission.MapVisibility, + StartingResources: mission.StartingResources, + WorldTwists: []user.WorldTwist{}, + Opponents: []user.Opponent{}, + OpponentsFor2PlayerCoop: []user.Opponent{}, + Rewards: []user.MissionRewards{ + { + Amount: 1, + Scaling: "None", + ItemId: unusedBlessing, + }, + }, + } + nodePosToIndex[col][node] = i + blessingIndexes[blessingLevel]++ + } + missionColumns[col] = missionsColumn + } + // Remove 1 predecessor for each pair that cross each other (as long as there are more than 1 predecessor) + for col := 1; col < len(missionColumns); col++ { + currentColumn := missionColumns[col] + currentColPosToIndex := nodePosToIndex[col] + previousColumn := missionColumns[col-1] + previousColPosToIndex := nodePosToIndex[col-1] + for pos := 0; pos < rows-1; pos++ { + var currentColumnPos *user.ChallengeMission + if index, exists := currentColPosToIndex[pos]; !exists { + continue + } else { + currentColumnPos = currentColumn[index] + } + var previousColumnPos *user.ChallengeMission + if index, exists := previousColPosToIndex[pos]; !exists { + continue + } else { + previousColumnPos = previousColumn[index] + } + nextPos := pos + 1 + var currentColumnNextPos *user.ChallengeMission + if index, exists := currentColPosToIndex[nextPos]; !exists { + continue + } else { + currentColumnNextPos = currentColumn[index] + } + var previousColumnNextPos *user.ChallengeMission + if index, exists := previousColPosToIndex[nextPos]; !exists { + continue + } else { + previousColumnNextPos = previousColumn[index] + } + nextPosPredecessorIndex := slices.Index(currentColumnPos.Predecessors, previousColumnNextPos.Id) + if nextPosPredecessorIndex == -1 { + continue + } + posPredecessorIndex := slices.Index(currentColumnNextPos.Predecessors, previousColumnPos.Id) + if posPredecessorIndex == -1 { + continue + } + if rand.Intn(2) == 0 { + currentColumnPos.Predecessors = slices.Delete(currentColumnPos.Predecessors, nextPosPredecessorIndex, nextPosPredecessorIndex+1) + } else { + currentColumnNextPos.Predecessors = slices.Delete(currentColumnNextPos.Predecessors, posPredecessorIndex, posPredecessorIndex+1) + } + } + } + return +} diff --git a/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/precomputed/precomputed.go b/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/precomputed/precomputed.go new file mode 100644 index 00000000..1b72eb02 --- /dev/null +++ b/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth/precomputed/precomputed.go @@ -0,0 +1,69 @@ +package precomputed + +import ( + mapset "github.com/deckarep/golang-set/v2" + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/playfab" +) + +type Blessing struct { + EffectName string + Rarity int +} + +func (a Blessing) GauntletItem() string { + return playfab.ItemName("Gauntlet", a.EffectName, a.Rarity) +} + +func blessingKeys(blessings []playfab.Blessings) (keys mapset.Set[Blessing]) { + keys = mapset.NewThreadUnsafeSet[Blessing]() + for _, blessing := range blessings { + // This assumes empty KnownRarities means all rarities are included + for _, rarity := range blessing.KnownRarities { + keys.Add(Blessing{blessing.EffectName, rarity}) + } + } + return +} + +func AllowedGauntletBlessings(gauntlet playfab.Gauntlet, knownBlessings []playfab.Blessings) (blessings map[int][]string) { + disallowedBlessings := blessingKeys(gauntlet.Rewards.ExcludeFromRegularRewards) + allBlessings := blessingKeys(knownBlessings) + allowedBlessings := allBlessings.Difference(disallowedBlessings) + blessingLevels := mapset.NewThreadUnsafeSet[int]() + for blessing := range allowedBlessings.Iter() { + blessingLevels.Add(blessing.Rarity) + } + blessings = make(map[int][]string, blessingLevels.Cardinality()) + for level := range blessingLevels.Iter() { + blessings[level] = []string{} + } + for blessing := range allowedBlessings.Iter() { + blessings[blessing.Rarity] = append(blessings[blessing.Rarity], blessing.GauntletItem()) + } + return blessings +} + +func PoolNamesToIndex(missionPools playfab.GauntletMissionPools) map[string]int { + poolNamesToId := make(map[string]int, len(missionPools)) + for index, pool := range missionPools { + poolNamesToId[pool.Name] = index + } + return poolNamesToId +} + +func PoolsIndexByDifficulty(gauntlet playfab.Gauntlet, poolNamesToIndex map[string]int) (poolsIndexes map[string][]int) { + poolsIndexes = make(map[string][]int) + for _, config := range gauntlet.LabyrinthConfigs { + poolDifficultyIndex := make([]int, len(config.ColumnConfigs)+1) + var column int + var columnConfig playfab.ColumnConfig + for column, columnConfig = range config.ColumnConfigs { + poolDifficultyIndex[column] = poolNamesToIndex[columnConfig.MissionPool] + } + poolDifficultyIndex[column+1] = poolNamesToIndex[config.BossMissionPool] + for _, difficulty := range config.ForGauntletDifficulties { + poolsIndexes[difficulty] = poolDifficultyIndex + } + } + return +} diff --git a/server/internal/models/athens/routes/playfab/cloudScriptFunction/cloudScriptFunction.go b/server/internal/models/athens/routes/playfab/cloudScriptFunction/cloudScriptFunction.go new file mode 100644 index 00000000..e1b49d7f --- /dev/null +++ b/server/internal/models/athens/routes/playfab/cloudScriptFunction/cloudScriptFunction.go @@ -0,0 +1,20 @@ +package cloudScriptFunction + +import ( + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/playfab/cloudScriptFunction/buildGauntletLabyrinth" + "github.com/luskaner/ageLANServer/server/internal/models/playfab" +) + +var Store map[string]playfab.CloudScriptFunction +var fns = []playfab.CloudScriptFunction{ + NewAwardMissionRewardsFunction(), + NewStartGauntletMissionFunction(), + buildGauntletLabyrinth.NewFunction(), +} + +func init() { + Store = make(map[string]playfab.CloudScriptFunction, len(fns)) + for _, fn := range fns { + Store[fn.Name()] = fn + } +} diff --git a/server/internal/models/athens/routes/playfab/cloudScriptFunction/startGauntletMission.go b/server/internal/models/athens/routes/playfab/cloudScriptFunction/startGauntletMission.go new file mode 100644 index 00000000..3c21be26 --- /dev/null +++ b/server/internal/models/athens/routes/playfab/cloudScriptFunction/startGauntletMission.go @@ -0,0 +1,49 @@ +package cloudScriptFunction + +import ( + "fmt" + + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" + "github.com/luskaner/ageLANServer/server/internal/models/playfab" +) + +type StartGauntletMissionParameters struct { + CdnPath string + MissionId string +} + +type StartGauntletMissionResult struct{} + +type StartGauntletMissionFunction struct { + *playfab.CloudScriptFunctionBase[StartGauntletMissionParameters, StartGauntletMissionResult] +} + +func (s *StartGauntletMissionFunction) RunTyped(_ models.Game, u models.User, parameters *StartGauntletMissionParameters) *StartGauntletMissionResult { + athensUser := u.(*user.User) + _ = athensUser.PlayfabData.WithReadWrite(func(data *user.Data) error { + progress := data.Challenge.Progress + if progress == nil { + return fmt.Errorf("no progress found") + } + if (*progress.Value).MissionBeingPlayedRightNow != parameters.MissionId { + progress.Update(func(progress *user.Progress) { + progress.MissionBeingPlayedRightNow = parameters.MissionId + }) + data.DataVersion++ + return nil + } + return fmt.Errorf("no updates needed") + }) + return nil +} + +func (s *StartGauntletMissionFunction) Name() string { + return "StartGauntletMission" +} + +func NewStartGauntletMissionFunction() *StartGauntletMissionFunction { + f := &StartGauntletMissionFunction{} + f.CloudScriptFunctionBase = playfab.NewCloudScriptFunctionBase[StartGauntletMissionParameters, StartGauntletMissionResult](f) + return f +} diff --git a/server/internal/models/athens/routes/playfab/gauntlet.go b/server/internal/models/athens/routes/playfab/gauntlet.go new file mode 100644 index 00000000..6b65128e --- /dev/null +++ b/server/internal/models/athens/routes/playfab/gauntlet.go @@ -0,0 +1,43 @@ +package playfab + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/luskaner/ageLANServer/server/internal/models/playfab" +) + +type ColumnConfig struct { + MissionPool string +} + +type LabyrinthConfig struct { + ForGauntletDifficulties []string + ColumnConfigs []ColumnConfig + BossMissionPool string +} + +type Rewards struct { + ExcludeFromRegularRewards []Blessings + PreferredFinalRewards []any +} + +type Gauntlet struct { + LabyrinthConfigs []LabyrinthConfig + Rewards Rewards +} + +func ReadGauntlet() (gauntlet Gauntlet) { + if f, err := os.Open(filepath.Join(playfab.BaseDir, "public-production", "2", "gauntlet.json")); err == nil { + defer func(f *os.File) { + _ = f.Close() + }(f) + if err = json.NewDecoder(f).Decode(&gauntlet); err != nil { + panic(err) + } + return gauntlet + } else { + panic(err) + } +} diff --git a/server/internal/models/athens/routes/playfab/gauntlet_mission_pools.go b/server/internal/models/athens/routes/playfab/gauntlet_mission_pools.go new file mode 100644 index 00000000..f9b93172 --- /dev/null +++ b/server/internal/models/athens/routes/playfab/gauntlet_mission_pools.go @@ -0,0 +1,31 @@ +package playfab + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" + "github.com/luskaner/ageLANServer/server/internal/models/playfab" +) + +type GauntletMissionPool struct { + Name string + Missions []user.ChallengeMission +} + +type GauntletMissionPools []GauntletMissionPool + +func ReadGauntletMissionPools() (gauntletMissionPools GauntletMissionPools) { + if f, err := os.Open(filepath.Join(playfab.BaseDir, "public-production", "2", "gauntlet_mission_pools.json")); err == nil { + defer func(f *os.File) { + _ = f.Close() + }(f) + if err = json.NewDecoder(f).Decode(&gauntletMissionPools); err != nil { + panic(err) + } + return gauntletMissionPools + } else { + panic(err) + } +} diff --git a/server/internal/models/athens/routes/playfab/items.go b/server/internal/models/athens/routes/playfab/items.go new file mode 100644 index 00000000..9480e24c --- /dev/null +++ b/server/internal/models/athens/routes/playfab/items.go @@ -0,0 +1,64 @@ +package playfab + +import ( + "fmt" + "strings" + "time" + + "github.com/luskaner/ageLANServer/server/internal/models/playfab" +) + +func addItem(name string, catalogItems map[string]playfab.CatalogItem, inventoryItems *[]playfab.InventoryItem) { + *inventoryItems = append( + *inventoryItems, + playfab.InventoryItem{ + Id: name, + StackId: "default", + Amount: 1, + Type: "catalogItem", + }, + ) + dateFormatted := time.Date(2024, 5, 2, 3, 34, 0, 0, time.UTC).Format(playfab.Iso8601Layout) + catalogItems[name] = + playfab.CatalogItem{ + Id: name, + Type: "catalogItem", + AlternateIds: []playfab.CatalogItemAlternativeId{ + { + "FriendlyId", + name, + }, + }, + FriendlyId: name, + Title: playfab.CatalogItemTitle{NEUTRAL: name}, + CreatorEntity: playfab.CatalogItemCreatorEntity{Id: "C15F9", Type: "title", TypeString: "title"}, + Platforms: []any{}, + Tags: []any{}, + CreationDate: dateFormatted, + LastModifiedDate: dateFormatted, + StartDate: dateFormatted, + Contents: []any{}, + Images: []any{}, + ItemReferences: []any{}, + DeepLinks: []any{}, + } +} + +func ItemName(category string, effectName string, rarity int) string { + return fmt.Sprintf("Item_%s_%s_%d", category, effectName, rarity) +} + +func Items(blessings []Blessings) (catalogItems map[string]playfab.CatalogItem, inventoryItems []playfab.InventoryItem) { + catalogItems = make(map[string]playfab.CatalogItem) + for _, b := range blessings { + for _, r := range b.KnownRarities { + if r > -1 { + addItem(ItemName("Season0", b.EffectName, r), catalogItems, &inventoryItems) + if strings.HasPrefix(b.EffectName, "GrantLegend") { + addItem(ItemName("", b.EffectName, r), catalogItems, &inventoryItems) + } + } + } + } + return +} diff --git a/server/internal/models/athens/user/data.go b/server/internal/models/athens/user/data.go new file mode 100644 index 00000000..48d8f11b --- /dev/null +++ b/server/internal/models/athens/user/data.go @@ -0,0 +1,136 @@ +package user + +import ( + "time" + + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/playfab/data" +) + +type PunchCardProgress struct { + Holes uint8 + DateOfMostRecentHolePunch data.CustomTime +} + +type MissionRewards struct { + Amount int + Scaling string + ItemId string +} + +type WorldTwist struct { + Id string + ArenaEffectName string + Title string + Description string + OwnerIcon string + OwnerPortrait string + Visualization string +} + +type Opponent struct { + Civ string + Team int + Personality string + DifficultyOffset int +} + +type ChallengeMission struct { + RowIndex int `json:"-"` + Id string + Predecessors []string + PositionX int + PositionY int + Visualization string + Map string + Size string + VictoryCondition string + GameType string + MapVisibility string + StartingResources string + AllowTitans bool + WorldTwists []WorldTwist + Opponents []Opponent + OpponentsFor2PlayerCoop []Opponent + Rewards []MissionRewards + MinimapImage string + MapPreviewImage string +} + +type Labyrinth struct { + Id int + Difficulty string + Missions []ChallengeMission +} + +type ProgressInventory struct { + SeasonId string + Item string + Rarity int +} + +type Progress struct { + Lives int + CompletedMissions []string + Inventory []ProgressInventory + MissionBeingPlayedRightNow string +} + +type Challenge struct { + Labyrinth *data.BaseValue[Labyrinth] + Progress *data.BaseValue[Progress] +} + +type StoryMission struct { + State string + RewardsAwarded string + CompletionCountEasy uint32 + CompletionCountMedium uint32 + CompletionCountHard uint32 +} + +type Data struct { + Challenge Challenge + StoryMissions map[string]data.BaseValue[StoryMission] + PunchCardProgress data.BaseValue[PunchCardProgress] + DataVersion uint32 +} + +type PlayfabUpgradableDefaultData struct { + models.InitialUpgradableDefaultData[*Data] +} + +func NewAvatarStatsUpgradableDefaultData() *PlayfabUpgradableDefaultData { + return &PlayfabUpgradableDefaultData{} +} + +func (p *PlayfabUpgradableDefaultData) Default() *Data { + lastUpdated := data.CustomTime{Time: time.Now(), Format: "2006-01-02T15:04:05.000Z"} + permission := "Private" + missions := make(map[string]data.BaseValue[StoryMission], len(storyMissions)) + for _, missionId := range storyMissions { + missions[missionId] = data.BaseValue[StoryMission]{ + LastUpdated: lastUpdated, + Permission: permission, + Value: &StoryMission{ + State: "Completed", + RewardsAwarded: "Hard", + CompletionCountHard: 1, + }, + } + } + return &Data{ + StoryMissions: missions, + PunchCardProgress: data.BaseValue[PunchCardProgress]{ + LastUpdated: lastUpdated, + Permission: permission, + Value: &PunchCardProgress{ + DateOfMostRecentHolePunch: data.CustomTime{ + Time: time.Date(2024, 5, 2, 3, 34, 0, 0, time.UTC), + Format: time.RFC3339, + }, + }, + }, + DataVersion: 0, + } +} diff --git a/server/internal/models/athens/user/user.go b/server/internal/models/athens/user/user.go new file mode 100644 index 00000000..32ed2785 --- /dev/null +++ b/server/internal/models/athens/user/user.go @@ -0,0 +1,76 @@ +package user + +import ( + "github.com/luskaner/ageLANServer/common" + "github.com/luskaner/ageLANServer/server/internal/models" +) + +var storyMissions = []string{ + "Mission_Season0_L0P0C1M1", + "Mission_Season0_L0P0C2M1", + "Mission_Season0_L0P0C3M1", + "Mission_Season0_L0P0C4M1", + "Mission_Season0_L0P0C5M1", + "Mission_Season0_L0P0C6M1", + "Mission_Season0_L0P0C6M2", + "Mission_Season0_L0P0C7M3", + "Mission_Season0_L0P0C7M2", + "Mission_Season0_L0P0C7M1", + "Mission_Season0_L0P0C8M2", + "Mission_Season0_L0P0C8M1", + "Mission_Season0_L0P0C9M3", + "Mission_Season0_L0P0C9M1", + "Mission_Season0_L0P0C9M2", + "Mission_Season0_L0P0C10M1", + "Mission_Season0_L0P1C1M1", + "Mission_Season0_L0P1C2M1", + "Mission_Season0_L0P1C2M2", + "Mission_Season0_L0P1C3M1", + "Mission_Season0_L0P1C3M2", + "Mission_Season0_L0P1C3M3", + "Mission_Season0_L0P1C4M2", + "Mission_Season0_L0P1C4M1", + "Mission_Season0_L0P1C5M1", + "Mission_Season0_L0P1C6M2", + "Mission_Season0_L0P1C6M1", + "Mission_Season0_L0P1C6M3", + "Mission_Season0_L0P1C7M1", + "Mission_Season0_L0P1C7M2", + "Mission_Season0_L0P1C8M2", + "Mission_Season0_L0P1C8M3", + "Mission_Season0_L0P1C9M2", + "Mission_Season0_L0P1C9M1", + "Mission_Season0_L0P1C10M1", +} + +type Users struct { + *models.MainUsers +} + +func (users *Users) Initialize() { + users.MainUsers = &models.MainUsers{ + GenerateFn: users.Generate, + } + users.MainUsers.Initialize() +} + +func (users *Users) Generate(_ string, persistentData *models.PersistentStringJsonMap, avatarStatsDefinitions models.AvatarStatDefinitions, identifier string, isXbox bool, platformUserId uint64, alias string) models.User { + d, err := models.NewPersistentJsonData[*Data]( + persistentData, + "playfab", + NewAvatarStatsUpgradableDefaultData(), + ) + if err != nil { + return nil + } + mainUser := users.MainUsers.Generate(common.GameAoM, persistentData, avatarStatsDefinitions, identifier, isXbox, platformUserId, alias) + return &User{ + MainUser: mainUser.(*models.MainUser), + PlayfabData: d, + } +} + +type User struct { + *models.MainUser + PlayfabData *models.PersistentJsonData[*Data] +} diff --git a/server/internal/models/avatarStats.go b/server/internal/models/avatarStats.go new file mode 100644 index 00000000..20f7de39 --- /dev/null +++ b/server/internal/models/avatarStats.go @@ -0,0 +1,165 @@ +package models + +import ( + "encoding/json" + "time" + + "github.com/luskaner/ageLANServer/common" + i "github.com/luskaner/ageLANServer/server/internal" +) + +type AvatarStat struct { + Id int32 `json:"-"` + // Read Value with AvatarStats.WithReadLock + Value int64 + // Read Metadata with AvatarStats.WithReadLock + Metadata json.RawMessage `json:",omitempty"` + // Read Metadata with AvatarStats.WithReadLock + LastUpdated time.Time +} + +func NewAvatarStat(id int32, value int64) AvatarStat { + return AvatarStat{ + Id: id, + Value: value, + LastUpdated: time.Now().UTC(), + } +} + +func (as *AvatarStat) SetValue(value int64) { + as.Value = value + as.LastUpdated = time.Now().UTC() +} + +func (as *AvatarStat) Encode(profileId int32) i.A { + return i.A{ + as.Id, + profileId, + as.Value, + as.Metadata, + as.LastUpdated.Unix(), + } +} + +type AvatarStats struct { + values *i.SafeMap[int32, AvatarStat] + locks *i.KeyRWMutex[int32] +} + +func (as *AvatarStats) MarshalJSON() ([]byte, error) { + data := make(map[int32]AvatarStat, as.values.Len()) + for stat := range as.values.Values() { + data[stat.Id] = stat + } + return json.Marshal(data) +} + +func (as *AvatarStats) UnmarshalJSON(b []byte) error { + var data map[int32]AvatarStat + if err := json.Unmarshal(b, &data); err != nil { + return err + } + as.values = i.NewSafeMap[int32, AvatarStat]() + as.locks = i.NewKeyRWMutex[int32]() + for id, stat := range data { + stat.Id = id + as.values.Store(stat.Id, stat, func(stored AvatarStat) bool { + return true + }) + } + return nil +} + +func (as *AvatarStats) GetStat(id int32) (AvatarStat, bool) { + return as.values.Load(id) +} + +// AddStat Must be ensured that the stat does not already exist +func (as *AvatarStats) AddStat(avatarStat AvatarStat) { + as.values.Store(avatarStat.Id, avatarStat, func(stored AvatarStat) bool { + return false + }) +} + +func (as *AvatarStats) Encode(profileId int32) i.A { + result := i.A{} + for val := range as.values.Values() { + result = append(result, val.Encode(profileId)) + } + return result +} + +func newAvatarStats(values map[int32]int64) *AvatarStats { + avatarStats := &AvatarStats{ + values: i.NewSafeMap[int32, AvatarStat](), + locks: i.NewKeyRWMutex[int32](), + } + for id, value := range values { + avatarStats.values.Store(id, AvatarStat{ + Id: id, + Value: value, + LastUpdated: time.Now().UTC(), + }, func(stored AvatarStat) bool { + return true + }) + } + return avatarStats +} + +type AvatarStatsUpgradableDefaultData struct { + InitialUpgradableDefaultData[*AvatarStats] + gameId string + avatarStatsDefinitions AvatarStatDefinitions +} + +func NewAvatarStatsUpgradableDefaultData(gameId string, definitions AvatarStatDefinitions) *AvatarStatsUpgradableDefaultData { + return &AvatarStatsUpgradableDefaultData{ + InitialUpgradableDefaultData: InitialUpgradableDefaultData[*AvatarStats]{}, + gameId: gameId, + avatarStatsDefinitions: definitions, + } +} + +func (a *AvatarStatsUpgradableDefaultData) Default() *AvatarStats { + var values map[string]int64 + switch a.gameId { + case common.GameAoE2: + // TODO: Remove the ones not needed + values = map[string]int64{ + "STAT_NUM_MVP_AWARDS": 0, + "STAT_HIGHEST_SCORE_TOTAL": 0, + "STAT_HIGHEST_SCORE_ECONOMIC": 0, + "STAT_HIGHEST_SCORE_TECHNOLOGY": 0, + "STAT_CAREER_UNITS_KILLED": 0, + "STAT_CAREER_UNITS_LOST": 0, + "STAT_CAREER_UNITS_CONVERTED": 0, + "STAT_CAREER_BUILDINGS_RAZED": 0, + "STAT_CAREER_BUILDINGS_LOST": 0, + "STAT_CAREER_NUM_CASTLES": 0, + "STAT_GAMES_PLAYED_ONLINE": 0, + "STAT_ELO_XRM_WINS": 0, + "STAT_POP_CAP_200_MP": 0, + "STAT_POP_PEAK_200_MP": 0, + "STAT_TOTAL_GAMES": 0, + } + case common.GameAoE3: + // FIXME: Is this even needed? + values = map[string]int64{ + "STAT_EVENT_EXPLORER_SKIN_CHALLENGE_14c": 16, + } + case common.GameAoM: + values = map[string]int64{ + "STAT_GAUNTLET_REWARD_XP": 2_147_483_647, + "STAT_GAUNTLET_REWARD_FAVOUR": 19_500, + } + default: + values = map[string]int64{} + } + intValues := make(map[int32]int64, len(values)) + for k, v := range values { + if id, ok := a.avatarStatsDefinitions.GetIdByName(k); ok { + intValues[id] = v + } + } + return newAvatarStats(intValues) +} diff --git a/server/internal/models/baseSession.go b/server/internal/models/baseSession.go new file mode 100644 index 00000000..844d5c13 --- /dev/null +++ b/server/internal/models/baseSession.go @@ -0,0 +1,137 @@ +package models + +import ( + "iter" + "slices" + "sync" + "time" + + "github.com/luskaner/ageLANServer/server/internal" +) + +func newExpiry(duration time.Duration) time.Time { + return time.Now().UTC().Add(duration) +} + +type BaseSession[T comparable, D any] struct { + id T + expiryLock sync.RWMutex + expiry time.Time + data *D +} + +func (session *BaseSession[T, D]) Expiry() time.Time { + return session.expiry +} + +func (session *BaseSession[T, D]) Id() T { + return session.id +} + +func (session *BaseSession[T, D]) Data() *D { + return session.data +} + +type BaseSessions[T comparable, D any] struct { + expiry time.Duration + store *internal.SafeMap[T, *BaseSession[T, D]] + sweeperTaskMu sync.Mutex + sweeperTaskStarted bool +} + +func (sessions *BaseSessions[T, D]) CreateSession(idGenFun func() T, data *D) (stored *BaseSession[T, D]) { + exp := newExpiry(sessions.expiry) + for exists := true; exists; { + info := &BaseSession[T, D]{ + id: idGenFun(), + expiry: exp, + data: data, + } + stored, exists = sessions.store.Store(info.id, info, func(stored *BaseSession[T, D]) bool { + return false + }) + } + sessions.sweeperTaskMu.Lock() + defer sessions.sweeperTaskMu.Unlock() + if !sessions.sweeperTaskStarted { + go sessions.startSweeper() + sessions.sweeperTaskStarted = true + } + return stored +} + +func (sessions *BaseSessions[T, D]) Get(id T) (*BaseSession[T, D], bool) { + return sessions.store.Load(id) +} + +func (sessions *BaseSessions[T, D]) Values() iter.Seq[*BaseSession[T, D]] { + return sessions.store.Values() +} + +func (sessions *BaseSessions[T, D]) Delete(id T) { + sessions.store.Delete(id) +} + +func (sessions *BaseSessions[T, D]) startSweeper() { + go func() { + var alreadyExpired []T + _, nextExpiration := sessions.nextExpiration() + ticker := time.NewTicker(nextExpiration) + defer ticker.Stop() + for { + select { + case <-ticker.C: + alreadyExpired, nextExpiration = sessions.nextExpiration() + ticker.Reset(nextExpiration) + for _, expired := range alreadyExpired { + sessions.store.Delete(expired) + } + } + } + }() +} + +func (sessions *BaseSessions[T, D]) nextExpiration() (alreadyExpired []T, nextExpiration time.Duration) { + var expirationTimes []time.Time + now := time.Now().UTC() + for sess := range sessions.store.Values() { + sess.expiryLock.RLock() + if sess.expiry.Before(now) { + alreadyExpired = append(alreadyExpired, sess.id) + } else { + expirationTimes = append(expirationTimes, sess.expiry) + } + sess.expiryLock.RUnlock() + } + if len(expirationTimes) > 0 { + slices.SortFunc(expirationTimes, func(a, b time.Time) int { + switch { + case a.Before(b): + return -1 + case a.After(b): + return 1 + default: + return 0 + } + }) + nextExpiration = expirationTimes[0].Sub(now) + } else { + nextExpiration = sessions.expiry + } + return +} + +func (sessions *BaseSessions[T, D]) ResetExpiryTimer(id T) { + if sess, ok := sessions.Get(id); ok { + sess.expiryLock.Lock() + defer sess.expiryLock.Unlock() + sess.expiry = newExpiry(sessions.expiry) + } +} + +func NewBaseSessions[T comparable, D any](expiry time.Duration) *BaseSessions[T, D] { + return &BaseSessions[T, D]{ + store: internal.NewSafeMap[T, *BaseSession[T, D]](), + expiry: expiry, + } +} diff --git a/server/internal/models/battleServer.go b/server/internal/models/battleServer.go index 29e6edcd..47f6fb04 100644 --- a/server/internal/models/battleServer.go +++ b/server/internal/models/battleServer.go @@ -12,6 +12,25 @@ import ( "github.com/luskaner/ageLANServer/server/internal" ) +type BattleServer interface { + SetLAN(lan bool) + SetIPv4(ipv4 string) + SetBsPort(bsPort int) + SetWebSocketPort(webSocketPort int) + SetOutOfBandPort(outOfBandPort int) + SetHasOobPort(hasOobPort bool) + SetBattleServerName(battleServerName string) + SetName(name string) + LAN() bool + Region() string + AppendName(encoded *internal.A) + EncodeLogin(r *http.Request) internal.A + EncodePorts() internal.A + EncodeAdvertisement(r *http.Request) internal.A + ResolveIPv4(r *http.Request) string + String() string +} + type MainBattleServer struct { battleServerConfig.BaseConfig `mapstructure:",squash"` lan *bool @@ -20,6 +39,34 @@ type MainBattleServer struct { lanMu sync.RWMutex } +func (battleServer *MainBattleServer) SetBattleServerName(battleServerName string) { + battleServer.battleServerName = battleServerName +} + +func (battleServer *MainBattleServer) SetHasOobPort(hasOobPort bool) { + battleServer.hasOobPort = hasOobPort +} + +func (battleServer *MainBattleServer) SetIPv4(ipv4 string) { + battleServer.IPv4 = ipv4 +} + +func (battleServer *MainBattleServer) SetBsPort(bsPort int) { + battleServer.BsPort = bsPort +} + +func (battleServer *MainBattleServer) SetWebSocketPort(webSocketPort int) { + battleServer.WebSocketPort = webSocketPort +} + +func (battleServer *MainBattleServer) SetOutOfBandPort(outOfBandPort int) { + battleServer.OutOfBandPort = outOfBandPort +} + +func (battleServer *MainBattleServer) SetName(name string) { + battleServer.Name = name +} + func (battleServer *MainBattleServer) LAN() bool { battleServer.lanMu.RLock() if battleServer.lan == nil { @@ -28,7 +75,7 @@ func (battleServer *MainBattleServer) LAN() bool { battleServer.lanMu.Lock() battleServer.lan = &lan defer battleServer.lanMu.Unlock() - if guid, err := uuid.Parse(battleServer.Region); err == nil && guid.Version() == 4 { + if guid, err := uuid.Parse(battleServer.BaseConfig.Region); err == nil && guid.Version() == 4 { lan = true } } else { @@ -47,9 +94,19 @@ func (battleServer *MainBattleServer) AppendName(encoded *internal.A) { } } +func (battleServer *MainBattleServer) SetLAN(enable bool) { + battleServer.lanMu.Lock() + defer battleServer.lanMu.Unlock() + battleServer.lan = &enable +} + +func (battleServer *MainBattleServer) Region() string { + return battleServer.BaseConfig.Region +} + func (battleServer *MainBattleServer) EncodeLogin(r *http.Request) internal.A { encoded := internal.A{ - battleServer.Region, + battleServer.BaseConfig.Region, } battleServer.AppendName(&encoded) encoded = append(encoded, battleServer.ResolveIPv4(r)) @@ -86,7 +143,7 @@ func (battleServer *MainBattleServer) ResolveIPv4(r *http.Request) string { func (battleServer *MainBattleServer) String() string { str := fmt.Sprintf( "Region: %s (Name: %s), IPv4: %s, Ports: ", - battleServer.Region, + battleServer.BaseConfig.Region, battleServer.Name, battleServer.IPv4, ) @@ -95,27 +152,36 @@ func (battleServer *MainBattleServer) String() string { return str } +type BattleServers interface { + Initialize(battleServers []BattleServer, haveOobPort bool, battleServerName string) + Iter() iter.Seq2[string, BattleServer] + Encode(r *http.Request) internal.A + Get(region string) (BattleServer, bool) + NewLANBattleServer(region string) BattleServer + NewBattleServer(region string) BattleServer +} + type MainBattleServers struct { - store *internal.ReadOnlyOrderedMap[string, *MainBattleServer] + store *internal.ReadOnlyOrderedMap[string, BattleServer] haveOobPort bool battleServerName string } -func (battleSrvs *MainBattleServers) Initialize(battleServers []*MainBattleServer, haveOobPort bool, battleServerName string) { +func (battleSrvs *MainBattleServers) Initialize(battleServers []BattleServer, haveOobPort bool, battleServerName string) { keyOrder := make([]string, len(battleServers)) - mapping := make(map[string]*MainBattleServer, len(battleServers)) + mapping := make(map[string]BattleServer, len(battleServers)) for i, bs := range battleServers { - battleServers[i].hasOobPort = haveOobPort - battleServers[i].battleServerName = battleServerName - keyOrder[i] = bs.Region + battleServers[i].SetHasOobPort(haveOobPort) + battleServers[i].SetBattleServerName(battleServerName) + keyOrder[i] = bs.Region() mapping[keyOrder[i]] = battleServers[i] } battleSrvs.battleServerName = battleServerName battleSrvs.haveOobPort = haveOobPort - battleSrvs.store = internal.NewReadOnlyOrderedMap[string, *MainBattleServer](keyOrder, mapping) + battleSrvs.store = internal.NewReadOnlyOrderedMap[string, BattleServer](keyOrder, mapping) } -func (battleSrvs *MainBattleServers) Iter() iter.Seq2[string, *MainBattleServer] { +func (battleSrvs *MainBattleServers) Iter() iter.Seq2[string, BattleServer] { return battleSrvs.store.Iter() } @@ -129,18 +195,17 @@ func (battleSrvs *MainBattleServers) Encode(r *http.Request) internal.A { return encoded } -func (battleSrvs *MainBattleServers) Get(region string) (*MainBattleServer, bool) { +func (battleSrvs *MainBattleServers) Get(region string) (BattleServer, bool) { return battleSrvs.store.Load(region) } -func (battleSrvs *MainBattleServers) NewLANBattleServer(region string) *MainBattleServer { +func (battleSrvs *MainBattleServers) NewLANBattleServer(region string) BattleServer { battleServer := battleSrvs.NewBattleServer(region) - lan := true - battleServer.lan = &lan + battleServer.SetLAN(true) return battleServer } -func (battleSrvs *MainBattleServers) NewBattleServer(region string) *MainBattleServer { +func (battleSrvs *MainBattleServers) NewBattleServer(region string) BattleServer { return &MainBattleServer{ BaseConfig: battleServerConfig.BaseConfig{ Region: region, diff --git a/server/internal/models/battleServerLoader.go b/server/internal/models/battleServerLoader.go index 01daf0f9..620aa68f 100644 --- a/server/internal/models/battleServerLoader.go +++ b/server/internal/models/battleServerLoader.go @@ -5,19 +5,17 @@ import ( "github.com/luskaner/ageLANServer/common" "github.com/luskaner/ageLANServer/common/battleServerConfig" - "github.com/spf13/viper" + i "github.com/luskaner/ageLANServer/server/internal" ) -var BattleServers = make(map[string][]*MainBattleServer) +var BattleServersStore = make(map[string][]BattleServer) -func InitializeBattleServers(gameId string) error { - var battleServers []*MainBattleServer - key := fmt.Sprintf("Games.%s.BattleServers", gameId) - if viper.IsSet(key) { - err := viper.UnmarshalKey(key, &battleServers) - if err != nil { - return err - } +func InitializeBattleServers(gameId string, configBattleServers []i.BattleServer) error { + var battleServers []BattleServer + for _, bs := range configBattleServers { + battleServers = append(battleServers, &MainBattleServer{ + BaseConfig: bs.BaseConfig, + }) } tmpBattleServer, err := battleServerConfig.Configs(gameId, true) if err != nil { @@ -31,6 +29,6 @@ func InitializeBattleServers(gameId string) error { if gameId == common.GameAoM && len(battleServers) == 0 { return fmt.Errorf("no battle server for AoM") } - BattleServers[gameId] = battleServers + BattleServersStore[gameId] = battleServers return nil } diff --git a/server/internal/models/chatChannel.go b/server/internal/models/chatChannel.go index c561aa70..755cb32d 100644 --- a/server/internal/models/chatChannel.go +++ b/server/internal/models/chatChannel.go @@ -7,17 +7,39 @@ import ( "github.com/luskaner/ageLANServer/server/internal" ) +type ChatChannel interface { + GetId() int32 + GetName() string + GetUsers() iter.Seq[User] + AddUser(user User, clientLibVersion uint16) (exists bool, encodedUsers internal.A) + RemoveUser(user User) bool + HasUser(user User) bool + Encode() internal.A +} + type MainChatChannel struct { Id int32 Name string - users *internal.SafeOrderedMap[int32, *MainUser] + users *internal.SafeOrderedMap[int32, User] +} + +func NewChatChannel(id int32, name string) *MainChatChannel { + return &MainChatChannel{ + Id: id, + Name: name, + users: internal.NewSafeOrderedMap[int32, User](), + } } func (channel *MainChatChannel) GetId() int32 { return channel.Id } -func (channel *MainChatChannel) encode() internal.A { +func (channel *MainChatChannel) GetName() string { + return channel.Name +} + +func (channel *MainChatChannel) Encode() internal.A { return internal.A{ channel.Id, channel.Name, @@ -26,8 +48,8 @@ func (channel *MainChatChannel) encode() internal.A { } } -func (channel *MainChatChannel) GetUsers() iter.Seq[*MainUser] { - return func(yield func(user *MainUser) bool) { +func (channel *MainChatChannel) GetUsers() iter.Seq[User] { + return func(yield func(user User) bool) { _, users := channel.users.Values() for v := range users { if !yield(v) { @@ -37,8 +59,8 @@ func (channel *MainChatChannel) GetUsers() iter.Seq[*MainUser] { } } -func (channel *MainChatChannel) AddUser(user *MainUser, clientLibVersion uint16) (exists bool, encodedUsers internal.A) { - exists, _ = channel.users.IterAndStore(user.GetId(), user, nil, func(length int, users iter.Seq2[int32, *MainUser]) { +func (channel *MainChatChannel) AddUser(user User, clientLibVersion uint16) (exists bool, encodedUsers internal.A) { + exists, _ = channel.users.IterAndStore(user.GetId(), user, nil, func(length int, users iter.Seq2[int32, User]) { i := 0 encodedUsers = make(internal.A, length) for _, el := range users { @@ -49,20 +71,27 @@ func (channel *MainChatChannel) AddUser(user *MainUser, clientLibVersion uint16) return } -func (channel *MainChatChannel) RemoveUser(user *MainUser) bool { +func (channel *MainChatChannel) RemoveUser(user User) bool { return channel.users.Delete(user.GetId()) } -func (channel *MainChatChannel) HasUser(user *MainUser) bool { +func (channel *MainChatChannel) HasUser(user User) bool { _, ok := channel.users.Load(user.GetId()) return ok } +type ChatChannels interface { + Initialize(chatChannels map[string]*MainChatChannel) + Encode() internal.A + GetById(id int32) (*MainChatChannel, bool) + Iter() iter.Seq2[int32, *MainChatChannel] +} + type MainChatChannels struct { index *internal.ReadOnlyOrderedMap[int32, *MainChatChannel] } -func (channels *MainChatChannels) Initialize(chatChannels map[string]MainChatChannel) { +func (channels *MainChatChannels) Initialize(chatChannels map[string]*MainChatChannel) { keys := make([]int32, len(chatChannels)) values := make(map[int32]*MainChatChannel, len(chatChannels)) j := 0 @@ -71,10 +100,9 @@ func (channels *MainChatChannels) Initialize(chatChannels map[string]MainChatCha if err != nil { panic(err) } - channel.users = internal.NewSafeOrderedMap[int32, *MainUser]() - channel.Id = int32(idInt) - keys[j] = channel.Id - values[channel.Id] = &channel + c := NewChatChannel(int32(idInt), channel.GetName()) + keys[j] = c.GetId() + values[c.GetId()] = c j++ } channels.index = internal.NewReadOnlyOrderedMap[int32, *MainChatChannel](keys, values) @@ -84,7 +112,7 @@ func (channels *MainChatChannels) Encode() internal.A { c := make(internal.A, channels.index.Len()) i := 0 for el := range channels.index.Values() { - c[i] = el.encode() + c[i] = el.Encode() i++ } return c diff --git a/server/internal/models/cloudfiles.go b/server/internal/models/cloudfiles.go index c187e5f7..10388c42 100644 --- a/server/internal/models/cloudfiles.go +++ b/server/internal/models/cloudfiles.go @@ -22,7 +22,7 @@ type CloudfilesIndex struct { type CloudFiles struct { baseFolder string Value map[string]CloudfilesIndex - Credentials *Credentials + Credentials Credentials } func (m *CloudFiles) GetByKey(key string) (string, *CloudfilesIndex, bool) { @@ -49,13 +49,12 @@ func BuildCloudfilesIndex(configFolder string, baseFolder string) *CloudFiles { } index := CloudFiles{ baseFolder: baseFolder, - Credentials: &Credentials{}, + Credentials: NewCredentials(), } err = json.Unmarshal(data, &index.Value) if err != nil { panic(err) } - index.Credentials.Initialize() for i, fileInfo := range index.Value { data, err = index.ReadFile(i) if err != nil { diff --git a/server/internal/models/credentials.go b/server/internal/models/credentials.go index 65e42d29..45b25d15 100644 --- a/server/internal/models/credentials.go +++ b/server/internal/models/credentials.go @@ -3,32 +3,14 @@ package models import ( "crypto/sha256" "encoding/base64" - "slices" - "sync" "time" i "github.com/luskaner/ageLANServer/server/internal" ) -const expiry = time.Minute * 5 +const credentialsExpiry = time.Minute * 5 -type Credentials struct { - store *i.SafeMap[string, *Credential] - sweeperTaskMu sync.Mutex - sweeperTaskStarted bool -} - -type Credential struct { - signature string - key string - expiry time.Time -} - -func (creds *Credentials) Initialize() { - creds.store = i.NewSafeMap[string, *Credential]() -} - -func (creds *Credentials) generateSignature() string { +func generateSignature() string { b := make([]byte, 32) i.WithRng(func(rand *i.RandReader) { for j := 0; j < len(b); j++ { @@ -39,86 +21,15 @@ func (creds *Credentials) generateSignature() string { return base64.StdEncoding.EncodeToString(hash[:]) } -func (creds *Credentials) CreateCredentials(key string) *Credential { - var storedCred *Credential - for exists := true; exists; { - info := &Credential{ - key: key, - signature: creds.generateSignature(), - expiry: time.Now().UTC().Add(expiry), - } - storedCred, exists = creds.store.Store(info.signature, info, func(_ *Credential) bool { - return false - }) - } - creds.sweeperTaskMu.Lock() - defer creds.sweeperTaskMu.Unlock() - if !creds.sweeperTaskStarted { - go creds.startSweeper() - creds.sweeperTaskStarted = true - } - return storedCred -} - -func (creds *Credentials) nextExpiration() (alreadyExpired []string, nextExpiration time.Duration) { - var expirationTimes []time.Time - now := time.Now().UTC() - for cred := range creds.store.Values() { - if cred.expiry.Before(now) { - alreadyExpired = append(alreadyExpired, cred.signature) - } else { - expirationTimes = append(expirationTimes, cred.expiry) - } - } - if len(expirationTimes) > 0 { - slices.SortFunc(expirationTimes, func(a, b time.Time) int { - switch { - case a.Before(b): - return -1 - case a.After(b): - return 1 - default: - return 0 - } - }) - nextExpiration = expirationTimes[0].Sub(now) - } else { - nextExpiration = expiry - } - return -} - -func (creds *Credentials) startSweeper() { - go func() { - var alreadyExpired []string - _, nextExpiration := creds.nextExpiration() - ticker := time.NewTicker(nextExpiration) - defer ticker.Stop() - for { - select { - case <-ticker.C: - alreadyExpired, nextExpiration = creds.nextExpiration() - ticker.Reset(nextExpiration) - for _, expired := range alreadyExpired { - creds.store.Delete(expired) - } - } - } - }() -} - -func (creds *Credentials) GetCredentials(signature string) (*Credential, bool) { - return creds.store.Load(signature) -} - -func (cred *Credential) GetExpiry() time.Time { - return cred.expiry -} +type credentialKey = string +type credentialValue = string +type Credentials = *BaseSessions[credentialKey, credentialValue] +type Credential = *BaseSession[credentialKey, credentialValue] -func (cred *Credential) GetSignature() string { - return cred.signature +func NewCredentials() Credentials { + return NewBaseSessions[string, string](credentialsExpiry) } -func (cred *Credential) GetKey() string { - return cred.key +func CreateCredential(creds Credentials, key *string) Credential { + return creds.CreateSession(generateSignature, key) } diff --git a/server/internal/models/game.go b/server/internal/models/game.go index e39b02a9..8ea7c960 100644 --- a/server/internal/models/game.go +++ b/server/internal/models/game.go @@ -3,12 +3,14 @@ package models import "net/http" type Game interface { - BattleServers() *MainBattleServers - Resources() *MainResources - Users() *MainUsers - Advertisements() *MainAdvertisements - ChatChannels() *MainChatChannels Title() string + Resources() Resources + LeaderboardDefinitions() LeaderboardDefinitions + BattleServers() BattleServers + Users() Users + Advertisements() Advertisements + ChatChannels() ChatChannels + Sessions() Sessions } func G(r *http.Request) Game { diff --git a/server/internal/models/initializer/initializer.go b/server/internal/models/initializer/initializer.go index 2966c8e3..ceec5346 100644 --- a/server/internal/models/initializer/initializer.go +++ b/server/internal/models/initializer/initializer.go @@ -2,6 +2,7 @@ package initializer import ( "github.com/luskaner/ageLANServer/common" + i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" "github.com/luskaner/ageLANServer/server/internal/models/age1" "github.com/luskaner/ageLANServer/server/internal/models/age2" @@ -11,8 +12,8 @@ import ( var Games = map[string]models.Game{} -func InitializeGame(gameId string) error { - if err := models.InitializeBattleServers(gameId); err != nil { +func InitializeGame(gameId string, configBattleServers []i.BattleServer) error { + if err := models.InitializeBattleServers(gameId, configBattleServers); err != nil { return err } var game models.Game diff --git a/server/internal/models/leaderboard.go b/server/internal/models/leaderboard.go new file mode 100644 index 00000000..92844684 --- /dev/null +++ b/server/internal/models/leaderboard.go @@ -0,0 +1,55 @@ +package models + +import i "github.com/luskaner/ageLANServer/server/internal" + +type LeaderboardDefinitions interface { + Initialize(leaderboards i.A) + AvatarStatDefinitions() AvatarStatDefinitions +} + +type MainLeaderboardDefinitions struct { + avatarStatDefinitions *MainAvatarStatDefinitions +} + +func (l *MainLeaderboardDefinitions) Initialize(leaderboards i.A) { + l.avatarStatDefinitions = newAvatarStatDefinitions(leaderboards) +} + +func (l *MainLeaderboardDefinitions) AvatarStatDefinitions() AvatarStatDefinitions { + return l.avatarStatDefinitions +} + +func newAvatarStatDefinitions(leaderboards i.A) *MainAvatarStatDefinitions { + avatarStats := leaderboards[8].(i.A) + l := &MainAvatarStatDefinitions{ + nameToId: make(map[string]int32, len(avatarStats)), + idToName: make(map[int32]string, len(avatarStats)), + } + for _, avatarStat := range avatarStats { + id := int32(avatarStat.(i.A)[0].(float64)) + name := avatarStat.(i.A)[1].(string) + l.nameToId[name] = id + l.idToName[id] = name + } + return l +} + +type AvatarStatDefinitions interface { + GetIdByName(name string) (id int32, ok bool) + GetNameById(id int32) (name string, ok bool) +} + +type MainAvatarStatDefinitions struct { + nameToId map[string]int32 + idToName map[int32]string +} + +func (as *MainAvatarStatDefinitions) GetIdByName(name string) (id int32, ok bool) { + id, ok = as.nameToId[name] + return +} + +func (as *MainAvatarStatDefinitions) GetNameById(id int32) (name string, ok bool) { + name, ok = as.idToName[id] + return +} diff --git a/server/internal/models/mainGame.go b/server/internal/models/mainGame.go index 07242ad5..4021dbc3 100644 --- a/server/internal/models/mainGame.go +++ b/server/internal/models/mainGame.go @@ -5,48 +5,80 @@ import ( ) type MainGame struct { - battleServers *MainBattleServers - resources *MainResources - users *MainUsers - advertisements *MainAdvertisements - chatChannels *MainChatChannels - title string + battleServers BattleServers + resources Resources + users Users + advertisements Advertisements + chatChannels ChatChannels + sessions Sessions + leaderboardDefinitions LeaderboardDefinitions + title string } -func CreateMainGame(gameId string, rssKeyedFilenames mapset.Set[string], battleServerHaveOobPort bool, battleServerName string) *MainGame { +func CreateMainGame(gameId string, battleServers BattleServers, resources Resources, leaderboardDefinitions LeaderboardDefinitions, users Users, + advertisements Advertisements, chatChannels ChatChannels, sessions Sessions, rssKeyedFilenames mapset.Set[string], + battleServerHaveOobPort bool, battleServerName string) Game { + if battleServers == nil { + battleServers = &MainBattleServers{} + } + if resources == nil { + resources = &MainResources{} + } + if users == nil { + users = &MainUsers{} + } + if advertisements == nil { + advertisements = &MainAdvertisements{} + } + if chatChannels == nil { + chatChannels = &MainChatChannels{} + } + if sessions == nil { + sessions = &MainSessions{} + } game := &MainGame{ - battleServers: &MainBattleServers{}, - resources: &MainResources{}, - users: &MainUsers{}, - advertisements: &MainAdvertisements{}, - chatChannels: &MainChatChannels{}, + battleServers: battleServers, + resources: resources, + users: users, + advertisements: advertisements, + chatChannels: chatChannels, + sessions: sessions, title: gameId, } - game.battleServers.Initialize(BattleServers[gameId], battleServerHaveOobPort, battleServerName) + game.battleServers.Initialize(BattleServersStore[gameId], battleServerHaveOobPort, battleServerName) game.resources.Initialize(gameId, rssKeyedFilenames) game.users.Initialize() game.advertisements.Initialize(game.users, game.battleServers) - game.chatChannels.Initialize(game.resources.ChatChannels) + game.chatChannels.Initialize(game.resources.ChatChannels()) + game.sessions.Initialize() + if leaderboards, ok := game.resources.ArrayFiles()["leaderboards.json"]; ok { + if leaderboardDefinitions == nil { + leaderboardDefinitions = &MainLeaderboardDefinitions{} + } + game.leaderboardDefinitions = leaderboardDefinitions + game.leaderboardDefinitions.Initialize(leaderboards) + } return game } func CreateGame(gameId string, rssKeyedFilenames mapset.Set[string], battleServerHaveOobPort bool, battleServerName string) Game { - return CreateMainGame(gameId, rssKeyedFilenames, battleServerHaveOobPort, battleServerName) + return CreateMainGame(gameId, nil, nil, nil, nil, nil, nil, + nil, rssKeyedFilenames, battleServerHaveOobPort, battleServerName) } -func (g *MainGame) Resources() *MainResources { +func (g *MainGame) Resources() Resources { return g.resources } -func (g *MainGame) Users() *MainUsers { +func (g *MainGame) Users() Users { return g.users } -func (g *MainGame) Advertisements() *MainAdvertisements { +func (g *MainGame) Advertisements() Advertisements { return g.advertisements } -func (g *MainGame) ChatChannels() *MainChatChannels { +func (g *MainGame) ChatChannels() ChatChannels { return g.chatChannels } @@ -54,6 +86,14 @@ func (g *MainGame) Title() string { return g.title } -func (g *MainGame) BattleServers() *MainBattleServers { +func (g *MainGame) BattleServers() BattleServers { return g.battleServers } + +func (g *MainGame) Sessions() Sessions { + return g.sessions +} + +func (g *MainGame) LeaderboardDefinitions() LeaderboardDefinitions { + return g.leaderboardDefinitions +} diff --git a/server/internal/models/message.go b/server/internal/models/message.go index 73fd6e4c..70195834 100644 --- a/server/internal/models/message.go +++ b/server/internal/models/message.go @@ -4,14 +4,25 @@ import ( i "github.com/luskaner/ageLANServer/server/internal" ) +type Message interface { + GetTime() int64 + GetBroadcast() bool + GetContent() string + GetType() uint8 + GetSender() User + GetReceivers() []User + GetAdvertisementId() int32 + Encode() i.A +} + type MainMessage struct { advertisementId int32 time int64 broadcast bool content string typ uint8 - sender *MainUser - receivers []*MainUser + sender User + receivers []User } func (message *MainMessage) GetTime() int64 { @@ -30,11 +41,11 @@ func (message *MainMessage) GetType() uint8 { return message.typ } -func (message *MainMessage) GetSender() *MainUser { +func (message *MainMessage) GetSender() User { return message.sender } -func (message *MainMessage) GetReceivers() []*MainUser { +func (message *MainMessage) GetReceivers() []User { return message.receivers } diff --git a/server/internal/models/peer.go b/server/internal/models/peer.go index ee88ce33..749c4614 100644 --- a/server/internal/models/peer.go +++ b/server/internal/models/peer.go @@ -11,28 +11,41 @@ type MainPeerMutable struct { Team int32 } +type Peer interface { + GetUserId() int32 + Encode() i.A + Invite(user User) bool + Uninvite(user User) bool + GetMutable() *MainPeerMutable + UpdateMutable(race int32, team int32) +} + type MainPeer struct { advertisementId int32 advertisementIp string userId int32 userStatId int32 mutable *atomic.Value - invites *i.SafeSet[*MainUser] + invites *i.SafeSet[User] } -func NewPeer(advertisementId int32, advertisementIp string, userId int32, userStatId int32, race int32, team int32) *MainPeer { +func NewPeer(advertisementId int32, advertisementIp string, userId int32, userStatId int32, race int32, team int32) Peer { peer := &MainPeer{ advertisementId: advertisementId, advertisementIp: advertisementIp, userId: userId, userStatId: userStatId, mutable: &atomic.Value{}, - invites: i.NewSafeSet[*MainUser](), + invites: i.NewSafeSet[User](), } peer.UpdateMutable(race, team) return peer } +func (peer *MainPeer) GetUserId() int32 { + return peer.userId +} + func (peer *MainPeer) GetMutable() *MainPeerMutable { return peer.mutable.Load().(*MainPeerMutable) } @@ -50,11 +63,11 @@ func (peer *MainPeer) Encode() i.A { } } -func (peer *MainPeer) Invite(user *MainUser) bool { +func (peer *MainPeer) Invite(user User) bool { return peer.invites.Store(user) } -func (peer *MainPeer) Uninvite(user *MainUser) bool { +func (peer *MainPeer) Uninvite(user User) bool { return peer.invites.Delete(user) } diff --git a/server/internal/models/persistentJsonData.go b/server/internal/models/persistentJsonData.go new file mode 100644 index 00000000..f3e32c21 --- /dev/null +++ b/server/internal/models/persistentJsonData.go @@ -0,0 +1,333 @@ +package models + +import ( + "encoding/json" + "errors" + "io" + "io/fs" + "os" + "sync" + + "github.com/luskaner/ageLANServer/common/fileLock" + "github.com/luskaner/ageLANServer/server/internal" +) + +type UpgradableData[T any] interface { + ObjectOfVersion(version uint32) any + CurrentVersion() uint32 + UpgradeToNextVersion(oldVersion uint32, oldObject any) T +} + +type InitialUpgradableData[T any] struct { +} + +func (i *InitialUpgradableData[T]) ObjectOfVersion(_ uint32) any { + panic("should not have been called") +} + +func (i *InitialUpgradableData[T]) CurrentVersion() uint32 { + return 0 +} + +func (i *InitialUpgradableData[T]) UpgradeToNextVersion(_ uint32, _ any) T { + panic("should not have been called") +} + +type DefaultUpgradableData[T any] interface { + UpgradableData[T] + Default() T +} + +func upgrade[T any](file *PersistentFile, version uint32, upgrader UpgradableData[T]) (err error, upgraded bool, data T) { + versionsToUpgrade := upgrader.CurrentVersion() - version + if versionsToUpgrade == 0 { + return + } + currentData := upgrader.ObjectOfVersion(version) + if err = readPersistentData(file, ¤tData); err != nil { + return + } + for versionsUpgraded := uint32(0); versionsUpgraded < versionsToUpgrade; versionsUpgraded++ { + currentData = upgrader.UpgradeToNextVersion(version+versionsUpgraded, currentData) + } + data = currentData.(T) + upgraded = true + return +} + +type InitialUpgradableDefaultData[T any] struct { + InitialUpgradableData[T] +} + +func openFile(p string) (existed bool, f *os.File, err error) { + if f, err = os.OpenFile(p, os.O_RDWR, 0644); err == nil { + existed = true + return + } else if errors.Is(err, fs.ErrNotExist) { + f, err = os.OpenFile(p, os.O_RDWR|os.O_CREATE, 0644) + } + return +} + +type PersistentFile struct { + lock *sync.Mutex + fileLock *fileLock.Lock + existed bool +} + +func (d *PersistentFile) Existed() bool { + return d.existed +} + +func (d *PersistentFile) unsafeSeekTop() (err error) { + _, err = d.fileLock.File.Seek(0, 0) + return +} + +func (d *PersistentFile) WithWriter(fn func(writer io.Writer) error) (err error) { + d.lock.Lock() + defer d.lock.Unlock() + if err = d.unsafeSeekTop(); err != nil { + return + } + if err = d.fileLock.File.Truncate(0); err != nil { + return + } + err = fn(d.fileLock.File) + _ = d.fileLock.File.Sync() + return err +} + +func (d *PersistentFile) WithReader(fn func(writer io.Reader) error) (err error) { + d.lock.Lock() + defer d.lock.Unlock() + if err = d.unsafeSeekTop(); err != nil { + return + } + return fn(d.fileLock.File) +} + +func (d *PersistentFile) Close() (err error) { + d.lock.Lock() + defer d.lock.Unlock() + return d.fileLock.Unlock() +} + +func NewPersistentData(path string) (data *PersistentFile, err error) { + var f *os.File + var existed bool + existed, f, err = openFile(path) + if err != nil { + return + } + lock := fileLock.Lock{} + if err = lock.Lock(f); err != nil { + _ = f.Close() + return + } + data = &PersistentFile{ + &sync.Mutex{}, + &lock, + existed, + } + return +} + +type jsonMetadata struct { + Version uint32 `json:"version"` +} + +type jsonDataWithMetadata[T any] struct { + Metadata jsonMetadata `json:"metadata"` + Data T `json:"data"` +} + +type jsonAnyDataWithMetadata struct { + Metadata jsonMetadata `json:"metadata"` + Data any `json:"data"` +} + +func readPersistentData(persistentFile *PersistentFile, data any) (err error) { + return persistentFile.WithReader(func(reader io.Reader) error { + return json.NewDecoder(reader).Decode(data) + }) +} + +type PersistentStringJsonMapRaw = internal.SafeMap[string, jsonDataWithMetadata[json.RawMessage]] +type PersistentStringJsonMapDataMap = internal.SafeMap[string, jsonAnyDataWithMetadata] + +type PersistentStringJsonMap struct { + file *PersistentFile + cachedRawData jsonDataWithMetadata[*PersistentStringJsonMapRaw] + currentData jsonDataWithMetadata[*PersistentStringJsonMapDataMap] + currentDataLock *internal.KeyRWMutex[string] +} + +func NewPersistentStringMap(path string, upgrader UpgradableData[*PersistentStringJsonMapRaw]) (persistentMap *PersistentStringJsonMap, err error) { + var file *PersistentFile + file, err = NewPersistentData(path) + if err != nil { + return + } + var initialRawData *PersistentStringJsonMapRaw + if file.Existed() { + var metadata jsonMetadata + if err = readPersistentData(file, &metadata); err != nil { + return + } + if metadata.Version > upgrader.CurrentVersion() { + _ = file.fileLock.Unlock() + err = errors.New("data version is newer than current version") + return + } else if localErr, upgraded, data := upgrade(file, metadata.Version, upgrader); localErr == nil && upgraded { + initialRawData = data + } else if localErr != nil { + _ = file.fileLock.Unlock() + err = localErr + return + } else { + if err = readPersistentData(file, &initialRawData); err != nil { + return + } + } + } else { + initialRawData = internal.NewSafeMap[string, jsonDataWithMetadata[json.RawMessage]]() + } + persistentMap = &PersistentStringJsonMap{ + file, + jsonDataWithMetadata[*PersistentStringJsonMapRaw]{ + Metadata: jsonMetadata{Version: upgrader.CurrentVersion()}, + Data: initialRawData, + }, + jsonDataWithMetadata[*PersistentStringJsonMapDataMap]{ + Metadata: jsonMetadata{Version: upgrader.CurrentVersion()}, + Data: internal.NewSafeMap[string, jsonAnyDataWithMetadata](), + }, + internal.NewKeyRWMutex[string](), + } + if !file.Existed() { + err = file.WithWriter(func(writer io.Writer) error { + return json.NewEncoder(writer).Encode(persistentMap.cachedRawData) + }) + } + return +} + +func psmjSet[T any](p *PersistentStringJsonMap, key string, value DefaultUpgradableData[T]) (err error) { + p.currentDataLock.Lock(key) + defer p.currentDataLock.Unlock(key) + var data T + var saveToCache bool + if currentVal, ok := p.cachedRawData.Data.Load(key); !ok { + data = value.Default() + saveToCache = true + } else if valueCurrentVersion := value.CurrentVersion(); currentVal.Metadata.Version > valueCurrentVersion { + return errors.New("data version is newer than current version") + } else if localErr, upgraded, d := upgrade(p.file, currentVal.Metadata.Version, value); localErr == nil && upgraded { + data = d + saveToCache = true + } else if localErr != nil { + err = localErr + return + } else if err = json.Unmarshal(currentVal.Data, &data); err != nil { + return + } + finalData := jsonAnyDataWithMetadata{ + jsonMetadata{value.CurrentVersion()}, + data, + } + if _, exists := (*p.currentData.Data).Store(key, finalData, func(_ jsonAnyDataWithMetadata) bool { + return false + }); exists { + return errors.New("key already exists") + } + if saveToCache { + cacheData := jsonDataWithMetadata[json.RawMessage]{ + Metadata: jsonMetadata{value.CurrentVersion()}, + } + if cacheData.Data, err = json.Marshal(data); err != nil { + return + } + p.cachedRawData.Data.Store(key, cacheData, func(stored jsonDataWithMetadata[json.RawMessage]) bool { + return true + }) + return p.file.WithWriter(func(writer io.Writer) error { + return json.NewEncoder(writer).Encode(p.cachedRawData) + }) + } + return nil +} + +func psmjFn[T any](p *PersistentStringJsonMap, key string, fn func(data T) error) (fullData jsonAnyDataWithMetadata, err error) { + var ok bool + fullData, ok = (*p.currentData.Data).Load(key) + if !ok { + err = errors.New("key does not exist") + return + } + err = fn(fullData.Data.(T)) + return +} + +func psmjWithReadOnly[T any](p *PersistentStringJsonMap, key string, fn func(data T) error) error { + p.currentDataLock.RLock(key) + defer p.currentDataLock.RUnlock(key) + _, err := psmjFn(p, key, fn) + return err +} + +func psmjWithReadWrite[T any](p *PersistentStringJsonMap, key string, fn func(data T) error) error { + p.currentDataLock.Lock(key) + defer p.currentDataLock.Unlock(key) + var fullData jsonAnyDataWithMetadata + var err error + if fullData, err = psmjFn(p, key, fn); err != nil { + return err + } + var dataBytes []byte + if dataBytes, err = json.Marshal(fullData.Data); err == nil { + _, _ = p.cachedRawData.Data.Store(key, jsonDataWithMetadata[json.RawMessage]{ + Metadata: fullData.Metadata, + Data: dataBytes, + }, func(_ jsonDataWithMetadata[json.RawMessage]) bool { + return true + }) + return p.file.WithWriter(func(writer io.Writer) error { + return json.NewEncoder(writer).Encode(p.cachedRawData) + }) + } else { + return err + } +} + +type PersistentJsonData[T any] struct { + persistentMap *PersistentStringJsonMap + key string +} + +func (p *PersistentJsonData[T]) WithReadOnly(fn func(data T) error) error { + return psmjWithReadOnly[T](p.persistentMap, p.key, fn) +} + +func (p *PersistentJsonData[T]) WithReadWrite(fn func(data T) error) error { + return psmjWithReadWrite[T](p.persistentMap, p.key, fn) +} + +func (p *PersistentJsonData[T]) MarshalJSON() (data []byte, err error) { + err = p.WithReadOnly(func(d T) error { + data, err = json.Marshal(d) + return err + }) + return +} + +func NewPersistentJsonData[T any](persistentMap *PersistentStringJsonMap, key string, upgrader DefaultUpgradableData[T]) (data *PersistentJsonData[T], err error) { + if err = psmjSet(persistentMap, key, upgrader); err != nil { + return + } + data = &PersistentJsonData[T]{ + persistentMap: persistentMap, + key: key, + } + return +} diff --git a/server/internal/models/playfab/cloudScriptFunction.go b/server/internal/models/playfab/cloudScriptFunction.go index 23de574d..409724b4 100644 --- a/server/internal/models/playfab/cloudScriptFunction.go +++ b/server/internal/models/playfab/cloudScriptFunction.go @@ -1,6 +1,36 @@ package playfab -type CloudScriptFunction[P any, R any] interface { - Run(P) R +import ( + "github.com/luskaner/ageLANServer/server/internal/models" +) + +type Named interface { Name() string } + +type CloudScriptFunction interface { + Named + Run(game models.Game, user models.User, parameters any) any + NewParameters() any +} + +type SpecificCloudScriptFunction[P any, R any] interface { + Named + RunTyped(game models.Game, user models.User, parameters *P) *R +} + +type CloudScriptFunctionBase[P any, R any] struct { + SpecificCloudScriptFunction[P, R] +} + +func NewCloudScriptFunctionBase[P any, R any](impl SpecificCloudScriptFunction[P, R]) *CloudScriptFunctionBase[P, R] { + return &CloudScriptFunctionBase[P, R]{SpecificCloudScriptFunction: impl} +} + +func (c *CloudScriptFunctionBase[P, R]) NewParameters() any { + return new(P) +} + +func (c *CloudScriptFunctionBase[P, R]) Run(game models.Game, user models.User, parameters any) any { + return c.RunTyped(game, user, parameters.(*P)) +} diff --git a/server/internal/models/playfab/data.go b/server/internal/models/playfab/data.go deleted file mode 100644 index 402a7284..00000000 --- a/server/internal/models/playfab/data.go +++ /dev/null @@ -1,43 +0,0 @@ -package playfab - -import ( - "encoding/json" - "time" -) - -const CustomTimeFormat = "2006-01-02T15:04:05.000Z" - -type CustomTime struct { - time.Time - Format string -} - -func (ct CustomTime) MarshalJSON() ([]byte, error) { - formatted := ct.Time.UTC().Format(CustomTimeFormat) - return json.Marshal(formatted) -} - -type ValueLike interface { - Prepare() error -} - -type Value[T any] struct { - Val *T `json:"-"` - Value string - LastUpdated CustomTime - Permission string -} - -func (v *Value[T]) Prepare() error { - v.LastUpdated = CustomTime{time.Now(), "2006-01-02T15:04:05.000Z"} - v.Permission = "Private" - if v.Val != nil { - if val, err := json.Marshal(v.Val); err == nil { - v.Value = string(val) - return nil - } else { - return err - } - } - return nil -} diff --git a/server/internal/models/playfab/data/data.go b/server/internal/models/playfab/data/data.go new file mode 100644 index 00000000..c9bc90d9 --- /dev/null +++ b/server/internal/models/playfab/data/data.go @@ -0,0 +1,75 @@ +package data + +import ( + "encoding/json" + "time" +) + +const CustomTimeFormat = "2006-01-02T15:04:05.000Z" + +type CustomTime struct { + time.Time + Format string +} + +func (ct CustomTime) Update() { + ct.Time = time.Now() +} + +func (ct CustomTime) MarshalJSON() ([]byte, error) { + formatted := ct.Time.UTC().Format(CustomTimeFormat) + return json.Marshal(formatted) +} + +type Value[T any] struct { + LastUpdated CustomTime + Permission string + Value *T +} + +func (v *Value[T]) MarshalJSON() ([]byte, error) { + if val, err := json.Marshal(v.Value); err == nil { + stringVal := string(val) + return json.Marshal(BaseValue[string]{ + LastUpdated: v.LastUpdated, + Permission: v.Permission, + Value: &stringVal, + }) + } else { + return nil, err + } +} + +type BaseValue[T any] Value[T] + +func (b *BaseValue[T]) UpdateLastUpdated() { + b.LastUpdated.Update() +} + +func (b *BaseValue[T]) Update(updateFn func(*T)) { + updateFn(b.Value) + b.UpdateLastUpdated() +} + +func (b *BaseValue[T]) ToValue() *Value[T] { + if b == nil { + return nil + } + return &Value[T]{ + LastUpdated: b.LastUpdated, + Permission: b.Permission, + Value: b.Value, + } +} + +func NewBaseValue[T any](permission string, value T) *BaseValue[T] { + return &BaseValue[T]{ + LastUpdated: CustomTime{Time: time.Now(), Format: "2006-01-02T15:04:05.000Z"}, + Permission: permission, + Value: &value, + } +} + +func NewPrivateBaseValue[T any](value T) *BaseValue[T] { + return NewBaseValue("Private", value) +} diff --git a/server/internal/models/playfab/session.go b/server/internal/models/playfab/session.go index 5c296837..97528f30 100644 --- a/server/internal/models/playfab/session.go +++ b/server/internal/models/playfab/session.go @@ -4,11 +4,68 @@ import ( "encoding/binary" "encoding/hex" "net/http" + "time" + "github.com/google/uuid" "github.com/luskaner/ageLANServer/server/internal" + "github.com/luskaner/ageLANServer/server/internal/models" ) -var sessions *internal.SafeMap[string, string] +const sessionDuration = 24 * time.Hour + +type SessionKey = string + +type SessionData struct { + playfabId string + entityToken string + user models.User +} + +func (s *SessionData) PlayfabId() string { + return s.playfabId +} + +func (s *SessionData) EntityToken() string { + return s.entityToken +} + +func (s *SessionData) User() models.User { + return s.user +} + +type MainSessions struct { + baseSessions *models.BaseSessions[SessionKey, SessionData] +} + +func (s *MainSessions) Initialize() { + s.baseSessions = models.NewBaseSessions[SessionKey, SessionData](sessionDuration) +} + +func (s *MainSessions) Create(users models.Users, steamUserId uint64) SessionKey { + if user, found := users.GetUserByPlatformUserId(false, steamUserId); !found { + return "" + } else { + sess := &SessionData{ + entityToken: uuid.NewString(), + user: user, + } + stored := s.baseSessions.CreateSession(generateId, sess) + sess.playfabId = stored.Id() + return sess.playfabId + } +} + +func (s *MainSessions) GetById(entityToken string) (*SessionData, bool) { + baseSess, exists := s.baseSessions.Get(entityToken) + if !exists { + return nil, false + } + return baseSess.Data(), true +} + +func (s *MainSessions) ResetExpiry(entityToken string) { + s.baseSessions.ResetExpiryTimer(entityToken) +} func generateId() string { bytes := make([]byte, 8) @@ -18,20 +75,15 @@ func generateId() string { return hex.EncodeToString(bytes) } -func init() { - sessions = internal.NewSafeMap[string, string]() -} - -func Id(session string) (playfabId string, ok bool) { - return sessions.Load(session) -} - -func AddSession(session string) string { - id := generateId() - sessions.Store(session, id, nil) - return id +func SessionOrPanic(r *http.Request) *SessionData { + sessAny, ok := session(r) + if !ok { + panic("Session should have been set already") + } + return sessAny } -func Session(r *http.Request) string { - return r.Header.Get("X-Entitytoken") +func session(r *http.Request) (*SessionData, bool) { + sess, ok := r.Context().Value("session").(*SessionData) + return sess, ok } diff --git a/server/internal/models/playfab/steamAppTicket.go b/server/internal/models/playfab/steamAppTicket.go new file mode 100644 index 00000000..6ec98556 --- /dev/null +++ b/server/internal/models/playfab/steamAppTicket.go @@ -0,0 +1,90 @@ +package playfab + +import ( + "encoding/binary" + "encoding/hex" + "errors" +) + +func readUint32(b []byte, off *int) (uint32, bool) { + if *off+4 > len(b) { + return 0, false + } + v := binary.LittleEndian.Uint32(b[*off : *off+4]) + *off += 4 + return v, true +} + +func readUint64(b []byte, off *int) (uint64, bool) { + if *off+8 > len(b) { + return 0, false + } + v := binary.LittleEndian.Uint64(b[*off : *off+8]) + *off += 8 + return v, true +} + +// ParseSteamIDHex decodes a ticket provided as a hex string and returns the SteamID (uint64). +func ParseSteamIDHex(ticketHex string) (uint64, error) { + data, err := hex.DecodeString(ticketHex) + if err != nil { + return 0, err + } + + off := 0 + + initialLen, ok := readUint32(data, &off) + if !ok { + return 0, errors.New("ticket too short") + } + + if initialLen == 20 { + // wrapper case: read wrapper fields like node parser + if _, ok := readUint64(data, &off); !ok { + return 0, errors.New("unexpected end (gcToken)") + } + // skip 8 + off += 8 + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (tokenGenerated)") + } + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (sessionheader)") + } + off += 8 + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (sessionExternalIP)") + } + off += 4 + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (clientConnectionTime)") + } + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (clientConnectionCount)") + } + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (ownership section length)") + } + } else { + // rewind the 4 bytes we read + off -= 4 + } + + // ownership ticket length + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (ownershipLength)") + } + + // version + if _, ok := readUint32(data, &off); !ok { + return 0, errors.New("unexpected end (version)") + } + + // steamID + steamID, ok := readUint64(data, &off) + if !ok { + return 0, errors.New("unexpected end (steamID)") + } + + return steamID, nil +} diff --git a/server/internal/models/profileMetadata.go b/server/internal/models/profileMetadata.go new file mode 100644 index 00000000..a720d22b --- /dev/null +++ b/server/internal/models/profileMetadata.go @@ -0,0 +1,25 @@ +package models + +import ( + "github.com/luskaner/ageLANServer/common" +) + +type AvatarMetadataUpgradableDefaultData struct { + InitialUpgradableDefaultData[*string] + gameId string +} + +func NewAvatarMetadataUpgradableDefaultData(gameId string) *AvatarMetadataUpgradableDefaultData { + return &AvatarMetadataUpgradableDefaultData{ + InitialUpgradableDefaultData: InitialUpgradableDefaultData[*string]{}, + gameId: gameId, + } +} + +func (p *AvatarMetadataUpgradableDefaultData) Default() *string { + var metadata string + if p.gameId == common.GameAoE3 || p.gameId == common.GameAoM { + metadata = `{"v":1,"twr":0,"wlr":0,"ai":1,"ac":0}` + } + return &metadata +} diff --git a/server/internal/models/resources.go b/server/internal/models/resources.go index 0ce6be33..d6aa0fce 100644 --- a/server/internal/models/resources.go +++ b/server/internal/models/resources.go @@ -10,56 +10,81 @@ import ( "strings" mapset "github.com/deckarep/golang-set/v2" + "github.com/luskaner/ageLANServer/common/paths" i "github.com/luskaner/ageLANServer/server/internal" ) -const resourceFolder = "resources" - -var configFolder = filepath.Join(resourceFolder, "config") -var ResponsesFolder = filepath.Join(resourceFolder, "responses") +var ResponsesFolder = filepath.Join(paths.ResourcesDir, "responses") +var userDataFolder = filepath.Join(paths.ResourcesDir, "userData") var CloudFolder = filepath.Join(ResponsesFolder, "cloud") +type Resources interface { + Initialize(gameId string, keyedFilenames mapset.Set[string]) + ReturnSignedAsset(name string, w *http.ResponseWriter, req *http.Request, keyedResponse bool) + LoginData() []i.A + ChatChannels() map[string]*MainChatChannel + ArrayFiles() map[string]i.A + CloudFiles() CloudFiles +} + type MainResources struct { keyedFilenames mapset.Set[string] - ChatChannels map[string]MainChatChannel - LoginData []i.A - ArrayFiles map[string]i.A - KeyedFiles map[string][]byte + chatChannels map[string]*MainChatChannel + loginData []i.A + arrayFiles map[string]i.A + keyedFiles map[string][]byte nameToSignature map[string]string - CloudFiles CloudFiles + cloudFiles CloudFiles } func (r *MainResources) Initialize(gameId string, keyedFilenames mapset.Set[string]) { - r.ArrayFiles = make(map[string]i.A) - r.KeyedFiles = make(map[string][]byte) + r.arrayFiles = make(map[string]i.A) + r.keyedFiles = make(map[string][]byte) r.nameToSignature = make(map[string]string) r.keyedFilenames = keyedFilenames + r.initializeUserData(gameId) r.initializeLogin(gameId) r.initializeChatChannels(gameId) r.initializeResponses(gameId) r.initializeCloud(gameId) } +func (r *MainResources) LoginData() []i.A { + return r.loginData +} + +func (r *MainResources) ChatChannels() map[string]*MainChatChannel { + return r.chatChannels +} + +func (r *MainResources) ArrayFiles() map[string]i.A { + return r.arrayFiles +} + +func (r *MainResources) CloudFiles() CloudFiles { + return r.cloudFiles +} + func (r *MainResources) initializeChatChannels(gameId string) { - data, err := os.ReadFile(filepath.Join(configFolder, gameId, "chatChannels.json")) + data, err := os.ReadFile(filepath.Join(paths.ConfigsPath, gameId, "chatChannels.json")) if err != nil { return } - err = json.Unmarshal(data, &r.ChatChannels) + err = json.Unmarshal(data, &r.chatChannels) if err != nil { panic(err) } } func (r *MainResources) initializeLogin(gameId string) { - data, err := os.ReadFile(filepath.Join(configFolder, gameId, "login.json")) + data, err := os.ReadFile(filepath.Join(paths.ConfigsPath, gameId, "login.json")) if err != nil { panic(err) } re := regexp.MustCompile(`"([^"]*)"`) matches := re.FindAllStringSubmatch(string(data), -1) for j := 0; j < len(matches)-1; j += 2 { - r.LoginData = append(r.LoginData, i.A{matches[j][1], matches[j+1][1]}) + r.loginData = append(r.loginData, i.A{matches[j][1], matches[j+1][1]}) } } @@ -79,23 +104,23 @@ func (r *MainResources) initializeResponses(gameId string) { matches := re.FindStringSubmatch(string(data)) if len(matches) == 1 { serverSignature := matches[1] - r.KeyedFiles[name] = data + r.keyedFiles[name] = data r.nameToSignature[name] = serverSignature } } else { var result i.A err = json.Unmarshal(data, &result) if err == nil { - r.ArrayFiles[name] = result + r.arrayFiles[name] = result } } } } func (r *MainResources) initializeCloud(gameId string) { - cloudfiles := BuildCloudfilesIndex(filepath.Join(configFolder, gameId), filepath.Join(CloudFolder, gameId)) + cloudfiles := BuildCloudfilesIndex(filepath.Join(paths.ConfigsPath, gameId), filepath.Join(CloudFolder, gameId)) if cloudfiles != nil { - r.CloudFiles = *cloudfiles + r.cloudFiles = *cloudfiles } } @@ -103,10 +128,10 @@ func (r *MainResources) ReturnSignedAsset(name string, w *http.ResponseWriter, r var serverSignature string var response any if keyedResponse { - response = r.KeyedFiles[name] + response = r.keyedFiles[name] serverSignature = r.nameToSignature[name] } else { - response = r.ArrayFiles[name] + response = r.arrayFiles[name] arrayResponse := response.(i.A) serverSignature = arrayResponse[len(arrayResponse)-1].(string) } @@ -128,3 +153,25 @@ func (r *MainResources) ReturnSignedAsset(name string, w *http.ResponseWriter, r i.JSON(w, ret) } } + +func (r *MainResources) initializeUserData(gameId string) { + ensureFolder(filepath.Join(userDataFolder, gameId)) +} + +func ensureFolder(path string) { + if err := os.MkdirAll(path, os.ModePerm); err != nil { + panic(err) + } +} + +func UserDataPath(gameId string, steam bool, platformUserid string) string { + var platform string + if steam { + platform = "STEAM" + } else { + platform = "XBOX" + } + folder := filepath.Join(userDataFolder, gameId) + ensureFolder(folder) + return filepath.Join(folder, fmt.Sprintf("%s_%s", platform, platformUserid)+".json") +} diff --git a/server/internal/models/session.go b/server/internal/models/session.go index bea52c5e..5afa39fd 100644 --- a/server/internal/models/session.go +++ b/server/internal/models/session.go @@ -2,71 +2,68 @@ package models import ( "net/http" - "sync" "time" "github.com/luskaner/ageLANServer/server/internal" ) -type Session struct { - id string - clientLibVersion uint16 - expiryTimer *time.Timer - expiryTimerLock sync.Mutex - userId int32 - gameId string - messageChan chan internal.A +type Session interface { + Id() SessionKey + GetUserId() int32 + GetClientLibVersion() uint16 + AddMessage(message internal.A) + WaitForMessages(ackNum uint) (uint, []internal.A) } -var sessionStore = internal.NewSafeMap[string, *Session]() +const sessionDuration = 5 * time.Minute -var ( - sessionLetters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") - sessionDuration = 5 * time.Minute -) +var sessionLetters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") func generateSessionId() string { sessionId := make([]rune, 30) internal.WithRng(func(rand *internal.RandReader) { - for i := range sessionId { - sessionId[i] = sessionLetters[rand.IntN(len(sessionLetters))] + for j := range sessionId { + sessionId[j] = sessionLetters[rand.IntN(len(sessionLetters))] } }) return string(sessionId) } -func (sess *Session) GetId() string { - return sess.id +type SessionData struct { + id SessionKey + clientLibVersion uint16 + userId int32 + messageChan chan internal.A } -func (sess *Session) GetUserId() int32 { - return sess.userId +func (s *SessionData) Id() SessionKey { + return s.id } -func (sess *Session) GetGameId() string { - return sess.gameId +func (s *SessionData) GetUserId() int32 { + return s.userId } -func (sess *Session) GetClientLibVersion() uint16 { - return sess.clientLibVersion +func (s *SessionData) GetClientLibVersion() uint16 { + return s.clientLibVersion } -func (sess *Session) AddMessage(message internal.A) { - sess.messageChan <- message +func (s *SessionData) AddMessage(message internal.A) { + s.messageChan <- message } -func (sess *Session) WaitForMessages(ackNum uint) (uint, []internal.A) { +func (s *SessionData) WaitForMessages(ackNum uint) (uint, []internal.A) { var results []internal.A timer := time.NewTimer(19 * time.Second) defer timer.Stop() for { select { - case msg := <-sess.messageChan: + case msg := <-s.messageChan: results = append(results, msg) - for len(results) < cap(sess.messageChan) { + for len(results) < cap(s.messageChan) { select { - case msg = <-sess.messageChan: + case msg = <-s.messageChan: results = append(results, msg) default: if len(results) > 0 { @@ -81,56 +78,72 @@ func (sess *Session) WaitForMessages(ackNum uint) (uint, []internal.A) { } } -func (sess *Session) Delete() { - func() { - sess.expiryTimerLock.Lock() - defer sess.expiryTimerLock.Unlock() - sess.expiryTimer.Stop() - }() - sessionStore.Delete(sess.id) +type SessionKey = string + +type Sessions interface { + Create(userId int32, clientLibVersion uint16) string + GetById(id string) (Session, bool) + GetByUserId(userId int32) (Session, bool) + Delete(id string) + ResetExpiry(id string) + Initialize() +} + +type MainSessions struct { + baseSessions *BaseSessions[SessionKey, SessionData] } -func (sess *Session) ResetExpiryTimer() { - sess.expiryTimerLock.Lock() - defer sess.expiryTimerLock.Unlock() - sess.expiryTimer.Reset(sessionDuration) +func (s *MainSessions) Initialize() { + s.baseSessions = NewBaseSessions[SessionKey, SessionData](sessionDuration) } -func CreateSession(gameId string, userId int32, clientLibVersion uint16) string { - sess := &Session{ +func (s *MainSessions) Create(userId int32, clientLibVersion uint16) string { + newId := generateSessionId() + sess := &SessionData{ + id: newId, userId: userId, - gameId: gameId, clientLibVersion: clientLibVersion, messageChan: make(chan internal.A, 100), } - defer func() { - sess.expiryTimer = time.AfterFunc(sessionDuration, func() { - sess.Delete() - }) - }() - for exists := true; exists; { - sess.id = generateSessionId() - _, exists = sessionStore.Store(sess.id, sess, func(_ *Session) bool { - return false - }) + s.baseSessions.CreateSession(func() string { return newId }, sess) + return newId +} + +func (s *MainSessions) GetById(id string) (Session, bool) { + baseSess, exists := s.baseSessions.Get(id) + if !exists { + return nil, false } - return sess.id + return baseSess.Data(), true } -func GetSessionById(sessionId string) (*Session, bool) { - return sessionStore.Load(sessionId) +func (s *MainSessions) getFirstByCondition(fn func(sess Session) bool) (Session, bool) { + for sess := range s.baseSessions.Values() { + if data := sess.Data(); fn(sess.Data()) { + return data, true + } + } + return nil, false } -func GetSessionByUserId(userId int32) (*Session, bool) { - for sess := range sessionStore.Values() { - if sess.userId == userId { - return sess, true +func (s *MainSessions) GetByUserId(userId int32) (Session, bool) { + for sess := range s.baseSessions.Values() { + if data := sess.Data(); data.GetUserId() == userId { + return data, true } } return nil, false } -func SessionOrPanic(r *http.Request) *Session { +func (s *MainSessions) Delete(id string) { + s.baseSessions.Delete(id) +} + +func (s *MainSessions) ResetExpiry(id string) { + s.baseSessions.ResetExpiryTimer(id) +} + +func SessionOrPanic(r *http.Request) Session { sessAny, ok := session(r) if !ok { panic("Session should have been set already") @@ -138,7 +151,7 @@ func SessionOrPanic(r *http.Request) *Session { return sessAny } -func session(r *http.Request) (*Session, bool) { - sess, ok := r.Context().Value("session").(*Session) +func session(r *http.Request) (Session, bool) { + sess, ok := r.Context().Value("session").(Session) return sess, ok } diff --git a/server/internal/models/user.go b/server/internal/models/user.go index 25ed3ab8..41e15333 100644 --- a/server/internal/models/user.go +++ b/server/internal/models/user.go @@ -10,49 +10,117 @@ import ( "sync/atomic" "time" - "github.com/luskaner/ageLANServer/common" i "github.com/luskaner/ageLANServer/server/internal" - "github.com/spf13/viper" ) +type User interface { + GetId() int32 + GetXbox() bool + GetStatId() int32 + GetProfileId() int32 + GetReliclink() int32 + GetAlias() string + GetPlatformPath() string + GetPlatformId() int + GetPlatformUserID() uint64 + GetExtraProfileInfo(clientLibVersion uint16) i.A + GetProfileInfo(includePresence bool, clientLibVersion uint16) i.A + GetPresence() int32 + SetPresence(presence int32) + GetAvatarMetadata() *PersistentJsonData[*string] + GetProfileExperience() uint32 + GetProfileLevel() uint16 + GetPlatformRelated() uint8 + GetAvatarStats() *PersistentJsonData[*AvatarStats] + GetPersistentData() *PersistentStringJsonMap + EncodeAvatarStats() i.A +} + type MainUser struct { - id int32 - statId int32 - alias string - platformUserId uint64 - profileId int32 - profileMetadata string - profileUintFlag1 uint8 - reliclink int32 - isXbox bool - // Only presence is dynamic - presence atomic.Int32 + id int32 + statId int32 + alias string + platformUserId uint64 + profileId int32 + profileExperience uint32 + reliclink int32 + isXbox bool + persistentData *PersistentStringJsonMap + // Dynamic from here + avatarMetadata *PersistentJsonData[*string] + presence atomic.Int32 + avatarStats *PersistentJsonData[*AvatarStats] +} + +func (u *MainUser) EncodeAvatarStats() i.A { + var result i.A + _ = u.GetAvatarStats().WithReadOnly(func(data *AvatarStats) error { + result = data.Encode(u.GetProfileId()) + return nil + }) + return result +} + +type Users interface { + Initialize() + GetOrCreateUser(gameId string, avatarStatsDefinitions AvatarStatDefinitions, remoteAddr string, remoteMacAddress string, isXbox bool, platformUserId uint64, alias string) User + GetUserByStatId(id int32) (User, bool) + GetUserById(id int32) (User, bool) + GetUserIds() func(func(int32) bool) + GetProfileInfo(includePresence bool, matches func(user User) bool, clientLibVersion uint16) []i.A + GetUserByPlatformUserId(xbox bool, id uint64) (User, bool) } type MainUsers struct { - store *i.SafeMap[string, *MainUser] + store *i.SafeMap[string, User] + GenerateFn func( + gameId string, + persistentData *PersistentStringJsonMap, + avatarStatsDefinitions AvatarStatDefinitions, + identifier string, + isXbox bool, + platformUserId uint64, + alias string, + ) User } func (users *MainUsers) Initialize() { - users.store = i.NewSafeMap[string, *MainUser]() + users.store = i.NewSafeMap[string, User]() + if users.GenerateFn == nil { + users.GenerateFn = users.Generate + } } -func (users *MainUsers) generate(identifier string, isXbox bool, platformUserId uint64, profileMetadata string, profileUIntFlag1 uint8, alias string) *MainUser { +func (users *MainUsers) Generate(gameId string, persistentData *PersistentStringJsonMap, avatarStatsDefinitions AvatarStatDefinitions, identifier string, isXbox bool, platformUserId uint64, alias string) User { hasher := fnv.New64a() _, _ = hasher.Write([]byte(identifier)) hsh := hasher.Sum(nil) seed := binary.BigEndian.Uint64(hsh) rng := rand.New(rand.NewPCG(seed, seed)) + var avatarStats *PersistentJsonData[*AvatarStats] + if avatarStatsDefinitions != nil { + avatarStats, _ = NewPersistentJsonData[*AvatarStats]( + persistentData, + "avatarStats", + NewAvatarStatsUpgradableDefaultData(gameId, avatarStatsDefinitions), + ) + } + avatarMetadata, _ := NewPersistentJsonData[*string]( + persistentData, + "avatarMetadata", + NewAvatarMetadataUpgradableDefaultData(gameId), + ) return &MainUser{ - id: rng.Int32(), - statId: rng.Int32(), - profileId: rng.Int32(), - profileMetadata: profileMetadata, - profileUintFlag1: profileUIntFlag1, - reliclink: rng.Int32(), - alias: alias, - platformUserId: platformUserId, - isXbox: isXbox, + id: rng.Int32(), + statId: rng.Int32(), + profileId: rng.Int32(), + avatarMetadata: avatarMetadata, + reliclink: rng.Int32(), + alias: alias, + platformUserId: platformUserId, + isXbox: isXbox, + avatarStats: avatarStats, + persistentData: persistentData, } } @@ -77,8 +145,8 @@ func generatePlatformUserIdXbox(rng *rand.Rand) uint64 { return uint64(rng.Int64N(9e15) + 1e15) } -func (users *MainUsers) GetOrCreateUser(gameId string, remoteAddr string, remoteMacAddress string, isXbox bool, platformUserId uint64, alias string) *MainUser { - if viper.GetBool("GeneratePlatformUserId") { +func (users *MainUsers) GetOrCreateUser(gameId string, avatarStatsDefinitions AvatarStatDefinitions, remoteAddr string, remoteMacAddress string, isXbox bool, platformUserId uint64, alias string) User { + if i.GeneratePlatformUserId { entropy := make([]byte, 16) macAddress, err := net.ParseMAC(remoteMacAddress) if err == nil { @@ -106,29 +174,42 @@ func (users *MainUsers) GetOrCreateUser(gameId string, remoteAddr string, remote } } identifier := getPlatformPath(isXbox, platformUserId) - var profileMetadata string - var profileUIntFlag1 uint8 - if gameId == common.GameAoE3 || gameId == common.GameAoM { - profileMetadata = `{"v":1,"twr":0,"wlr":0,"ai":1,"ac":0}` - profileUIntFlag1 = 1 - } - newUser := users.generate(identifier, isXbox, platformUserId, profileMetadata, profileUIntFlag1, alias) - mainUser, _ := users.store.LoadOrStore(identifier, newUser) + mainUser, _ := users.store.LoadOrStoreFn( + identifier, + func() User { + persistentData, _ := NewPersistentStringMap( + UserDataPath(gameId, !isXbox, strconv.FormatUint(platformUserId, 10)), + &InitialUpgradableData[*PersistentStringJsonMapRaw]{}, + ) + return users.GenerateFn( + gameId, + persistentData, + avatarStatsDefinitions, + identifier, + isXbox, + platformUserId, + alias, + ) + }, + ) return mainUser } -func (users *MainUsers) GetUserByStatId(id int32) (*MainUser, bool) { - for u := range users.store.Values() { - if u.statId == id { - return u, true - } - } - return nil, false +func (users *MainUsers) GetUserByStatId(id int32) (User, bool) { + return users.getFirst(func(u User) bool { return u.GetStatId() == id }) } -func (users *MainUsers) GetUserById(id int32) (*MainUser, bool) { +func (users *MainUsers) GetUserById(id int32) (User, bool) { + return users.getFirst(func(u User) bool { return u.GetId() == id }) +} + +func (users *MainUsers) GetUserByPlatformUserId(xbox bool, id uint64) (User, bool) { + return users.getFirst(func(u User) bool { return u.GetXbox() == xbox && u.GetPlatformUserID() == id }) +} + +func (users *MainUsers) getFirst(fn func(u User) bool) (User, bool) { for u := range users.store.Values() { - if u.id == id { + if fn(u) { return u, true } } @@ -145,7 +226,7 @@ func (users *MainUsers) GetUserIds() func(func(int32) bool) { } } -func (users *MainUsers) GetProfileInfo(includePresence bool, matches func(user *MainUser) bool, clientLibVersion uint16) []i.A { +func (users *MainUsers) GetProfileInfo(includePresence bool, matches func(user User) bool, clientLibVersion uint16) []i.A { var presenceData = make([]i.A, 0) for u := range users.store.Values() { if matches(u) { @@ -155,10 +236,22 @@ func (users *MainUsers) GetProfileInfo(includePresence bool, matches func(user * return presenceData } +func (u *MainUser) GetPersistentData() *PersistentStringJsonMap { + return u.persistentData +} + +func (u *MainUser) GetAvatarStats() *PersistentJsonData[*AvatarStats] { + return u.avatarStats +} + func (u *MainUser) GetId() int32 { return u.id } +func (u *MainUser) GetXbox() bool { + return u.isXbox +} + func (u *MainUser) GetStatId() int32 { return u.statId } @@ -238,7 +331,7 @@ func (u *MainUser) GetProfileInfo(includePresence bool, clientLibVersion uint16) time.Date(2024, 5, 2, 3, 34, 0, 0, time.UTC).Unix(), u.GetId(), u.GetPlatformPath(), - u.GetProfileMetadata(), + u.GetAvatarMetadata(), u.GetAlias(), } if clientLibVersion >= 190 { @@ -248,9 +341,9 @@ func (u *MainUser) GetProfileInfo(includePresence bool, clientLibVersion uint16) profileInfo, "", u.GetStatId(), - u.GetProfileUintFlag1(), - 1, - u.GetProfileUintFlag2(), + u.GetProfileExperience(), + u.GetProfileLevel(), + u.GetPlatformRelated(), nil, strconv.FormatUint(u.GetPlatformUserID(), 10), u.GetPlatformId(), @@ -270,18 +363,22 @@ func (u *MainUser) SetPresence(presence int32) { u.presence.Store(presence) } -func (u *MainUser) GetProfileMetadata() string { - return u.profileMetadata -} - -func (u *MainUser) GetProfileUintFlag1() uint8 { - return u.profileUintFlag1 +func (u *MainUser) GetAvatarMetadata() *PersistentJsonData[*string] { + return u.avatarMetadata } -func (u *MainUser) GetProfileUintFlag2() uint8 { +func (u *MainUser) GetPlatformRelated() uint8 { var value uint8 if u.isXbox { value = 3 } return value } + +func (u *MainUser) GetProfileLevel() uint16 { + return 9_999 +} + +func (u *MainUser) GetProfileExperience() uint32 { + return 0 +} diff --git a/server/internal/routes/apiAgeOfEmpires/textmoderation/textmoderation.go b/server/internal/routes/apiAgeOfEmpires/textmoderation/textmoderation.go index b661d079..f39c4d95 100644 --- a/server/internal/routes/apiAgeOfEmpires/textmoderation/textmoderation.go +++ b/server/internal/routes/apiAgeOfEmpires/textmoderation/textmoderation.go @@ -1,7 +1,6 @@ package textmoderation import ( - "encoding/json" "net/http" i "github.com/luskaner/ageLANServer/server/internal" @@ -25,7 +24,7 @@ type textModerationResponse struct { func TextModeration(w http.ResponseWriter, r *http.Request) { var req textModerationRequest - err := json.NewDecoder(r.Body).Decode(&req) + err := i.Bind(r, &req) if err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) } diff --git a/server/internal/routes/cacert.pem/cacertPem.go b/server/internal/routes/cacert.pem/cacertPem.go index f97a2cb9..4b4f1a96 100644 --- a/server/internal/routes/cacert.pem/cacertPem.go +++ b/server/internal/routes/cacert.pem/cacertPem.go @@ -19,18 +19,18 @@ func CacertPem(w http.ResponseWriter, r *http.Request) { if folder == "" { http.NotFound(w, r) return + } + + var file string + if common.SelfSignedCertGame(models.G(r).Title()) { + file = common.SelfSignedCert + } else { + file = common.CACert + } + path := filepath.Join(folder, file) + if _, err := os.Stat(path); os.IsNotExist(err) { + http.NotFound(w, r) } else { - var file string - if common.SelfSignedCertGame(models.G(r).Title()) { - file = common.SelfSignedCert - } else { - file = common.CACert - } - path := filepath.Join(folder, file) - if _, err := os.Stat(path); os.IsNotExist(err) { - http.NotFound(w, r) - } else { - http.ServeFile(w, r, path) - } + http.ServeFile(w, r, path) } } diff --git a/server/internal/routes/cloudfiles/cloudfiles.go b/server/internal/routes/cloudfiles/cloudfiles.go index 7f828974..9ed6c593 100644 --- a/server/internal/routes/cloudfiles/cloudfiles.go +++ b/server/internal/routes/cloudfiles/cloudfiles.go @@ -33,8 +33,8 @@ func generateRequestId() string { func Cloudfiles(w http.ResponseWriter, r *http.Request) { key := strings.Join(strings.Split(r.URL.Path, "/")[2:], "/") - cloudfiles := models.G(r).Resources().CloudFiles - info, exists := cloudfiles.Credentials.GetCredentials(r.URL.Query().Get("sig")) + cloudfiles := models.G(r).Resources().CloudFiles() + info, exists := (*cloudfiles.Credentials).Get(r.URL.Query().Get("sig")) if !exists { http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -43,7 +43,7 @@ func Cloudfiles(w http.ResponseWriter, r *http.Request) { filename, file, ok := cloudfiles.GetByKey(key) if ok { - if file.Key != info.GetKey() { + if file.Key != *info.Data() { http.Error(w, "Incorrect signature", http.StatusForbidden) return } diff --git a/server/internal/routes/game/account/findProfiles.go b/server/internal/routes/game/account/findProfiles.go index f5a61b44..cb5d1d7d 100644 --- a/server/internal/routes/game/account/findProfiles.go +++ b/server/internal/routes/game/account/findProfiles.go @@ -16,7 +16,7 @@ func FindProfiles(w http.ResponseWriter, r *http.Request) { } game := models.G(r) sess := models.SessionOrPanic(r) - profileInfo := game.Users().GetProfileInfo(true, func(currentUser *models.MainUser) bool { + profileInfo := game.Users().GetProfileInfo(true, func(currentUser models.User) bool { return strings.Contains(strings.ToLower(currentUser.GetAlias()), name) }, sess.GetClientLibVersion()) i.JSON(&w, i.A{0, profileInfo}) diff --git a/server/internal/routes/game/account/findProfilesByPlatformID.go b/server/internal/routes/game/account/findProfilesByPlatformID.go index 7bc5996f..efb21f05 100644 --- a/server/internal/routes/game/account/findProfilesByPlatformID.go +++ b/server/internal/routes/game/account/findProfilesByPlatformID.go @@ -1,32 +1,30 @@ package account import ( - "encoding/json" "net/http" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" ) +type findProfilesByPlatformIDRequest struct { + PlatformIDs i.Json[[]uint64] `schema:"platformIDs"` +} + func FindProfilesByPlatformID(w http.ResponseWriter, r *http.Request) { - platformIdsStr := r.PostFormValue("platformIDs") - if len(platformIdsStr) < 1 { - i.JSON(&w, i.A{2, i.A{}}) - return - } - var platformIds []uint64 - err := json.Unmarshal([]byte(platformIdsStr), &platformIds) + var req findProfilesByPlatformIDRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2, i.A{}}) return } - platformIdsMap := make(map[uint64]interface{}, len(platformIds)) - for _, platformId := range platformIds { + platformIdsMap := make(map[uint64]any, len(req.PlatformIDs.Data)) + for _, platformId := range req.PlatformIDs.Data { platformIdsMap[platformId] = struct{}{} } game := models.G(r) sess := models.SessionOrPanic(r) - profileInfo := game.Users().GetProfileInfo(true, func(currentUser *models.MainUser) bool { + profileInfo := game.Users().GetProfileInfo(true, func(currentUser models.User) bool { _, ok := platformIdsMap[currentUser.GetPlatformUserID()] return ok }, sess.GetClientLibVersion()) diff --git a/server/internal/routes/game/account/getProfileName.go b/server/internal/routes/game/account/getProfileName.go index 10808545..43dea8f6 100644 --- a/server/internal/routes/game/account/getProfileName.go +++ b/server/internal/routes/game/account/getProfileName.go @@ -1,32 +1,30 @@ package account import ( - "encoding/json" "net/http" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" ) +type getProfileNameRequest struct { + ProfileIDs i.Json[[]int32] `schema:"profile_ids"` +} + func GetProfileName(w http.ResponseWriter, r *http.Request) { - profileIdsStr := r.URL.Query().Get("profile_ids") - if len(profileIdsStr) < 1 { - i.JSON(&w, i.A{2, i.A{}}) - return - } - var profileIds []int32 - err := json.Unmarshal([]byte(profileIdsStr), &profileIds) + var req getProfileNameRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2, i.A{}}) return } - profileIdsMap := make(map[int32]interface{}, len(profileIds)) - for _, platformId := range profileIds { + profileIdsMap := make(map[int32]any, len(req.ProfileIDs.Data)) + for _, platformId := range req.ProfileIDs.Data { profileIdsMap[platformId] = struct{}{} } game := models.G(r) sess := models.SessionOrPanic(r) - profileInfo := game.Users().GetProfileInfo(false, func(currentUser *models.MainUser) bool { + profileInfo := game.Users().GetProfileInfo(false, func(currentUser models.User) bool { _, ok := profileIdsMap[currentUser.GetId()] return ok }, sess.GetClientLibVersion()) diff --git a/server/internal/routes/game/account/setAvatarMetadata.go b/server/internal/routes/game/account/setAvatarMetadata.go index 7aa75ba0..66643a57 100644 --- a/server/internal/routes/game/account/setAvatarMetadata.go +++ b/server/internal/routes/game/account/setAvatarMetadata.go @@ -4,8 +4,25 @@ import ( "net/http" i "github.com/luskaner/ageLANServer/server/internal" + "github.com/luskaner/ageLANServer/server/internal/models" ) -func SetAvatarMetadata(w http.ResponseWriter, _ *http.Request) { - i.JSON(&w, i.A{2, i.A{}}) +type setAvatarMetadataRequest struct { + Metadata string `json:"metaData"` +} + +func SetAvatarMetadata(w http.ResponseWriter, r *http.Request) { + var req setAvatarMetadataRequest + err := i.Bind(r, &req) + if err != nil { + i.JSON(&w, i.A{2, i.A{}}) + return + } + sess := models.SessionOrPanic(r) + u, _ := models.G(r).Users().GetUserById(sess.GetUserId()) + _ = u.GetAvatarMetadata().WithReadWrite(func(data *string) error { + *data = req.Metadata + return nil + }) + i.JSON(&w, i.A{0, u.GetProfileInfo(false, sess.GetClientLibVersion())}) } diff --git a/server/internal/routes/game/advertisement/findAdvertisements.go b/server/internal/routes/game/advertisement/findAdvertisements.go index 2b58b5cd..56415c85 100644 --- a/server/internal/routes/game/advertisement/findAdvertisements.go +++ b/server/internal/routes/game/advertisement/findAdvertisements.go @@ -31,7 +31,7 @@ func findAdvResp(errorCode int, advs i.A) i.A { return resp } -func findAdvertisements(w http.ResponseWriter, r *http.Request, length int, offset int, ongoing bool, lanRegions map[string]struct{}, extraCheck func(*models.MainAdvertisement) bool) { +func findAdvertisements(w http.ResponseWriter, r *http.Request, length int, offset int, ongoing bool, lanRegions map[string]struct{}, extraCheck func(models.Advertisement) bool) { var q searchQuery if err := i.Bind(r, &q); err != nil { i.JSON(&w, findAdvResp(2, i.A{})) @@ -41,20 +41,20 @@ func findAdvertisements(w http.ResponseWriter, r *http.Request, length int, offs title := game.Title() sess := models.SessionOrPanic(r) currentUserId := sess.GetUserId() - var battleServers *models.MainBattleServers + var battleServers models.BattleServers if len(lanRegions) == 0 { battleServers = game.BattleServers() } - var tagsCheck func(*models.MainAdvertisement) bool + var tagsCheck func(models.Advertisement) bool if battleServers != nil && (title == common.GameAoE2 || title == common.GameAoM) { ok, numericTags, stringTags := parseTags(r) if ok { - tagsCheck = func(adv *models.MainAdvertisement) bool { + tagsCheck = func(adv models.Advertisement) bool { return adv.UnsafeMatchesTags(numericTags, stringTags) } } } - advs := game.Advertisements().LockedFindAdvertisementsEncoded(title, length, offset, true, func(adv *models.MainAdvertisement) bool { + advs := game.Advertisements().LockedFindAdvertisementsEncoded(title, length, offset, true, func(adv models.Advertisement) bool { peers := adv.GetPeers() _, isPeer := peers.Load(currentUserId) var matchesBattleServer bool diff --git a/server/internal/routes/game/advertisement/findObservableAdvertisements.go b/server/internal/routes/game/advertisement/findObservableAdvertisements.go index 82a8a5fd..8a228cc3 100644 --- a/server/internal/routes/game/advertisement/findObservableAdvertisements.go +++ b/server/internal/routes/game/advertisement/findObservableAdvertisements.go @@ -27,7 +27,7 @@ func FindObservableAdvertisements(w http.ResponseWriter, r *http.Request) { return } } - findAdvertisements(w, r, q.Length, q.Offset, true, nil, func(advertisement *models.MainAdvertisement) bool { + findAdvertisements(w, r, q.Length, q.Offset, true, nil, func(advertisement models.Advertisement) bool { return advertisement.UnsafeGetObserversEnabled() }) } diff --git a/server/internal/routes/game/advertisement/getAdvertisements.go b/server/internal/routes/game/advertisement/getAdvertisements.go index eb2372db..eff15b40 100644 --- a/server/internal/routes/game/advertisement/getAdvertisements.go +++ b/server/internal/routes/game/advertisement/getAdvertisements.go @@ -1,7 +1,6 @@ package advertisement import ( - "encoding/json" "net/http" "slices" @@ -16,19 +15,22 @@ func getAdvResp(errorCode int, advs i.A) i.A { } } +type getAdvertisementsRequest struct { + MatchIDs i.Json[[]int32] `schema:"match_ids"` +} + func GetAdvertisements(w http.ResponseWriter, r *http.Request) { - matchIdsStr := r.URL.Query().Get("match_ids") - var advsIds []int32 - err := json.Unmarshal([]byte(matchIdsStr), &advsIds) + var req getAdvertisementsRequest + err := i.Bind(r, &req) if err != nil { - i.JSON(&w, getAdvResp(2, i.A{})) + i.JSON(&w, i.A{2, i.A{}}) return } game := models.G(r) title := game.Title() advertisements := game.Advertisements() - advs := advertisements.LockedFindAdvertisementsEncoded(title, 0, 0, false, func(adv *models.MainAdvertisement) bool { - return slices.Contains(advsIds, adv.GetId()) + advs := advertisements.LockedFindAdvertisementsEncoded(title, 0, 0, false, func(adv models.Advertisement) bool { + return slices.Contains(req.MatchIDs.Data, adv.GetId()) }) if advs == nil { i.JSON(&w, getAdvResp(0, i.A{})) diff --git a/server/internal/routes/game/advertisement/host.go b/server/internal/routes/game/advertisement/host.go index ebb02c51..269ac6e1 100644 --- a/server/internal/routes/game/advertisement/host.go +++ b/server/internal/routes/game/advertisement/host.go @@ -9,7 +9,7 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/game/advertisement/shared" ) -func returnError(battleServers *models.MainBattleServers, gameId string, r *http.Request, w *http.ResponseWriter) { +func returnError(battleServers models.BattleServers, gameId string, r *http.Request, w *http.ResponseWriter) { battleServer := battleServers.NewBattleServer("") response := encodeHostResponse( gameId, @@ -25,7 +25,7 @@ func returnError(battleServers *models.MainBattleServers, gameId string, r *http i.JSON(w, response) } -func encodeHostResponse(gameTitle string, errorCode int, advId int32, battleServer *models.MainBattleServer, r *http.Request, relayRegion string, encodedPeers []i.A, metadata string, description string) i.A { +func encodeHostResponse(gameTitle string, errorCode int, advId int32, battleServer models.BattleServer, r *http.Request, relayRegion string, encodedPeers []i.A, metadata string, description string) i.A { response := i.A{ errorCode, advId, diff --git a/server/internal/routes/game/advertisement/join.go b/server/internal/routes/game/advertisement/join.go index 16632ac8..c4d91d66 100644 --- a/server/internal/routes/game/advertisement/join.go +++ b/server/internal/routes/game/advertisement/join.go @@ -13,13 +13,13 @@ type JoinRequest struct { Password string `schema:"password"` } -func joinReturnError(battleServers *models.MainBattleServers, r *http.Request, w http.ResponseWriter) { +func joinReturnError(battleServers models.BattleServers, r *http.Request, w http.ResponseWriter) { battleServer := battleServers.NewBattleServer("") response := encodeJoinResponse(2, "", battleServer, r, i.A{}) i.JSON(&w, response) } -func encodeJoinResponse(errorCode int, ip string, battleServer *models.MainBattleServer, r *http.Request, peerEncoded i.A) i.A { +func encodeJoinResponse(errorCode int, ip string, battleServer models.BattleServer, r *http.Request, peerEncoded i.A) i.A { response := i.A{ errorCode, ip, diff --git a/server/internal/routes/game/advertisement/startObserving.go b/server/internal/routes/game/advertisement/startObserving.go index 232e8357..f3c65983 100644 --- a/server/internal/routes/game/advertisement/startObserving.go +++ b/server/internal/routes/game/advertisement/startObserving.go @@ -9,13 +9,13 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/game/advertisement/shared" ) -func joinReturnStartObservingError(battleServers *models.MainBattleServers, r *http.Request, w http.ResponseWriter) { +func joinReturnStartObservingError(battleServers models.BattleServers, r *http.Request, w http.ResponseWriter) { battleServer := battleServers.NewBattleServer("") response := encodeStartObservingResponse(2, battleServer, r, i.A{}, i.A{}, 0) i.JSON(&w, response) } -func encodeStartObservingResponse(errorCode int, battleServer *models.MainBattleServer, r *http.Request, userIdsInt i.A, userIdsStr i.A, startTime int64) i.A { +func encodeStartObservingResponse(errorCode int, battleServer models.BattleServer, r *http.Request, userIdsInt i.A, userIdsStr i.A, startTime int64) i.A { response := i.A{ errorCode, battleServer.ResolveIPv4(r), diff --git a/server/internal/routes/game/advertisement/update.go b/server/internal/routes/game/advertisement/update.go index cde84780..cde324d1 100644 --- a/server/internal/routes/game/advertisement/update.go +++ b/server/internal/routes/game/advertisement/update.go @@ -31,7 +31,7 @@ func Update(w http.ResponseWriter, r *http.Request) { var response i.A var ok bool advertisements.WithWriteLock(q.Id, func() { - var adv *models.MainAdvertisement + var adv models.Advertisement adv, ok = advertisements.GetAdvertisement(q.Id) if !ok { return @@ -40,7 +40,7 @@ func Update(w http.ResponseWriter, r *http.Request) { if gameTitle != common.GameAoE2 && gameTitle != common.GameAoM { q.Joinable = true } - advertisements.UpdateUnsafe(adv, &q) + adv.UnsafeUpdate(&q) if gameTitle != common.GameAoE2 && gameTitle != common.GameAoM { adv.UnsafeUpdatePlatformSessionId(q.PsnSessionId) } diff --git a/server/internal/routes/game/advertisement/updatePlatformLobbyID.go b/server/internal/routes/game/advertisement/updatePlatformLobbyID.go index 26cb7035..659ec6e3 100644 --- a/server/internal/routes/game/advertisement/updatePlatformLobbyID.go +++ b/server/internal/routes/game/advertisement/updatePlatformLobbyID.go @@ -34,7 +34,7 @@ func updatePlatformID(w *http.ResponseWriter, r *http.Request, idKey string) { } idValueUint := uint64(idValue) advertisements.WithWriteLock(req.MatchID, func() { - var adv *models.MainAdvertisement + var adv models.Advertisement adv, ok = advertisements.GetAdvertisement(req.MatchID) if !ok { return @@ -56,9 +56,10 @@ func updatePlatformID(w *http.ResponseWriter, r *http.Request, idKey string) { i.JSON(w, i.A{2}) return } + sessions := game.Sessions() message := i.A{req.MatchID, metadata, idValueUint} for peerId := range peersId { - if currentSess, ok := models.GetSessionByUserId(peerId); ok { + if currentSess, ok := sessions.GetByUserId(peerId); ok { wss.SendOrStoreMessage( currentSess, "PlatformSessionUpdateMessage", diff --git a/server/internal/routes/game/advertisement/updateState.go b/server/internal/routes/game/advertisement/updateState.go index 51dc622a..489c4822 100644 --- a/server/internal/routes/game/advertisement/updateState.go +++ b/server/internal/routes/game/advertisement/updateState.go @@ -28,11 +28,11 @@ func UpdateState(w http.ResponseWriter, r *http.Request) { battleServers := game.BattleServers() var ok bool var peersLen int - var peers iter.Seq2[int32, *models.MainPeer] + var peers iter.Seq2[int32, models.Peer] var advStartTime int64 var advEncoded i.A advertisements.WithWriteLock(q.AdvertisementId, func() { - var adv *models.MainAdvertisement + var adv models.Advertisement adv, ok = game.Advertisements().GetAdvertisement(q.AdvertisementId) if !ok { i.JSON(&w, i.A{2}) @@ -51,11 +51,12 @@ func UpdateState(w http.ResponseWriter, r *http.Request) { userIdStr := make([]i.A, peersLen) races := make([]i.A, peersLen) challengeProgress := make([]i.A, peersLen) - sessions := make([]*models.Session, peersLen) + sessions := make([]models.Session, peersLen) + gameSessions := game.Sessions() j := 0 for userId, peer := range peers { - var sess *models.Session - sess, ok = models.GetSessionByUserId(userId) + var sess models.Session + sess, ok = gameSessions.GetByUserId(userId) if !ok { continue } diff --git a/server/internal/routes/game/advertisement/updateTags.go b/server/internal/routes/game/advertisement/updateTags.go index d7a2aefd..6e42bc60 100644 --- a/server/internal/routes/game/advertisement/updateTags.go +++ b/server/internal/routes/game/advertisement/updateTags.go @@ -1,8 +1,6 @@ package advertisement import ( - "encoding/json" - "fmt" "net/http" i "github.com/luskaner/ageLANServer/server/internal" @@ -11,17 +9,10 @@ import ( ) type tagRequest struct { - NumericTagNames string `schema:"numericTagNames"` - NumericTagValues string `schema:"numericTagValues"` - StringTagNames string `schema:"stringTagNames"` - StringTagValues string `schema:"stringTagValues"` -} - -type tags struct { - NumericTagNames []string - NumericTagValues []int32 - StringTagNames []string - StringTagValues []string + NumericTagNames i.Json[[]string] `schema:"numericTagNames"` + NumericTagValues i.Json[[]int32] `schema:"numericTagValues"` + StringTagNames i.Json[[]string] `schema:"stringTagNames"` + StringTagValues i.Json[[]string] `schema:"stringTagValues"` } func parseTags(r *http.Request) (ok bool, numericTags map[string]int32, stringTags map[string]string) { @@ -29,38 +20,16 @@ func parseTags(r *http.Request) (ok bool, numericTags map[string]int32, stringTa if err := i.Bind(r, &t); err != nil { return } - if t.NumericTagNames == "" { - t.NumericTagNames = "[]" - } - if t.NumericTagValues == "" { - t.NumericTagValues = "[]" - } - if t.StringTagNames == "" { - t.StringTagNames = "[]" - } - if t.StringTagValues == "" { - t.StringTagValues = "[]" - } - var at tags - jsonText := fmt.Sprintf(`{ - "NumericTagNames": %s, - "NumericTagValues": %s, - "StringTagNames": %s, - "StringTagValues": %s -}`, t.NumericTagNames, t.NumericTagValues, t.StringTagNames, t.StringTagValues) - if err := json.Unmarshal([]byte(jsonText), &at); err != nil { - return - } - if (len(at.StringTagNames) != len(at.StringTagValues)) || (len(at.NumericTagNames) != len(at.NumericTagValues)) { + if (len(t.StringTagNames.Data) != len(t.StringTagValues.Data)) || (len(t.NumericTagNames.Data) != len(t.NumericTagValues.Data)) { return } - numericTags = make(map[string]int32, len(at.NumericTagNames)) - stringTags = make(map[string]string, len(at.StringTagNames)) - for j := 0; j < len(at.NumericTagNames); j++ { - numericTags[at.NumericTagNames[j]] = at.NumericTagValues[j] + numericTags = make(map[string]int32, len(t.NumericTagNames.Data)) + stringTags = make(map[string]string, len(t.StringTagNames.Data)) + for j := 0; j < len(t.NumericTagNames.Data); j++ { + numericTags[t.NumericTagNames.Data[j]] = t.NumericTagValues.Data[j] } - for j := 0; j < len(at.StringTagNames); j++ { - stringTags[at.StringTagNames[j]] = at.StringTagValues[j] + for j := 0; j < len(t.StringTagNames.Data); j++ { + stringTags[t.StringTagNames.Data[j]] = t.StringTagValues.Data[j] } ok = true return diff --git a/server/internal/routes/game/automatch2/getAutomatchMap.go b/server/internal/routes/game/automatch2/getAutomatchMap.go index 579be1c7..2358b406 100644 --- a/server/internal/routes/game/automatch2/getAutomatchMap.go +++ b/server/internal/routes/game/automatch2/getAutomatchMap.go @@ -8,5 +8,5 @@ import ( ) func GetAutomatchMap(w http.ResponseWriter, r *http.Request) { - i.JSON(&w, models.G(r).Resources().ArrayFiles["automatchMaps.json"]) + i.JSON(&w, models.G(r).Resources().ArrayFiles()["automatchMaps.json"]) } diff --git a/server/internal/routes/game/chat/joinChannel.go b/server/internal/routes/game/chat/joinChannel.go index 5bc11892..eb93541d 100644 --- a/server/internal/routes/game/chat/joinChannel.go +++ b/server/internal/routes/game/chat/joinChannel.go @@ -9,20 +9,19 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/wss" ) +type chatroomRequest struct { + ChatroomID int32 `schema:"chatroomID"` +} + func JoinChannel(w http.ResponseWriter, r *http.Request) { - // FIXME: Channels might show duplicate users (including the count) - chatChannelIdStr := r.FormValue("chatroomID") - if chatChannelIdStr == "" { - i.JSON(&w, i.A{2, "", 0, i.A{}}) - return - } - chatChannelId, err := strconv.ParseInt(chatChannelIdStr, 10, 32) + var req chatroomRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2, "", 0, i.A{}}) return } game := models.G(r) - chatChannel, ok := game.ChatChannels().GetById(int32(chatChannelId)) + chatChannel, ok := game.ChatChannels().GetById(req.ChatroomID) if !ok { i.JSON(&w, i.A{2, "", 0, i.A{}}) return @@ -39,11 +38,13 @@ func JoinChannel(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{2, "", 0, i.A{}}) return } + chatChannelIdStr := strconv.Itoa(int(req.ChatroomID)) i.JSON(&w, i.A{0, chatChannelIdStr, 0, encodedUsers}) + sessions := game.Sessions() staticResponse := i.A{chatChannelIdStr, i.A{0, user.GetProfileInfo(false, sess.GetClientLibVersion())}} for userId := range users.GetUserIds() { - var existingUserSession *models.Session - existingUserSession, ok = models.GetSessionByUserId(userId) + var existingUserSession models.Session + existingUserSession, ok = sessions.GetByUserId(userId) if ok { wss.SendOrStoreMessage( existingUserSession, diff --git a/server/internal/routes/game/chat/leaveChannel.go b/server/internal/routes/game/chat/leaveChannel.go index 39314d91..031ba15e 100644 --- a/server/internal/routes/game/chat/leaveChannel.go +++ b/server/internal/routes/game/chat/leaveChannel.go @@ -9,13 +9,13 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/wss" ) -func NotifyLeaveChannel(users *models.MainUsers, user *models.MainUser, chatChannelId int32, clientLibVersion uint16) { +func NotifyLeaveChannel(sessions models.Sessions, users models.Users, user models.User, chatChannelId int32, clientLibVersion uint16) { staticResponse := i.A{strconv.Itoa(int(chatChannelId)), user.GetProfileInfo(false, clientLibVersion)} for userId := range users.GetUserIds() { if userId == user.GetId() { continue } - existingUserSession, ok := models.GetSessionByUserId(userId) + existingUserSession, ok := sessions.GetByUserId(userId) if ok { wss.SendOrStoreMessage( existingUserSession, @@ -27,18 +27,14 @@ func NotifyLeaveChannel(users *models.MainUsers, user *models.MainUser, chatChan } func LeaveChannel(w http.ResponseWriter, r *http.Request) { - chatChannelIdStr := r.FormValue("chatroomID") - if chatChannelIdStr == "" { - i.JSON(&w, i.A{2}) - return - } - chatChannelId, err := strconv.ParseInt(chatChannelIdStr, 10, 32) + var req chatroomRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2}) return } game := models.G(r) - chatChannel, ok := game.ChatChannels().GetById(int32(chatChannelId)) + chatChannel, ok := game.ChatChannels().GetById(req.ChatroomID) if !ok { i.JSON(&w, i.A{2}) return @@ -51,5 +47,5 @@ func LeaveChannel(w http.ResponseWriter, r *http.Request) { return } i.JSON(&w, i.A{0}) - NotifyLeaveChannel(users, user, chatChannel.GetId(), sess.GetClientLibVersion()) + NotifyLeaveChannel(game.Sessions(), users, user, chatChannel.GetId(), sess.GetClientLibVersion()) } diff --git a/server/internal/routes/game/chat/sendText.go b/server/internal/routes/game/chat/sendText.go index b6407833..fc9802be 100644 --- a/server/internal/routes/game/chat/sendText.go +++ b/server/internal/routes/game/chat/sendText.go @@ -9,24 +9,24 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/wss" ) +type textRequest struct { + Message string `schema:"message"` +} + +type sendTextRequest struct { + chatroomRequest + textRequest +} + func SendText(w http.ResponseWriter, r *http.Request) { - text := r.FormValue("message") - if text == "" { - i.JSON(&w, i.A{2}) - return - } - chatChannelIdStr := r.FormValue("chatroomID") - if chatChannelIdStr == "" { - i.JSON(&w, i.A{2}) - return - } - chatChannelId, err := strconv.ParseInt(chatChannelIdStr, 10, 32) + var req sendTextRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2}) return } game := models.G(r) - chatChannel, ok := game.ChatChannels().GetById(int32(chatChannelId)) + chatChannel, ok := game.ChatChannels().GetById(req.ChatroomID) if !ok { i.JSON(&w, i.A{2}) return @@ -38,10 +38,11 @@ func SendText(w http.ResponseWriter, r *http.Request) { return } i.JSON(&w, i.A{0}) - staticResponse := i.A{chatChannelIdStr, strconv.Itoa(int(user.GetId())), "", text} + sessions := game.Sessions() + staticResponse := i.A{strconv.Itoa(int(req.ChatroomID)), strconv.Itoa(int(user.GetId())), "", req.Message} for existingUser := range chatChannel.GetUsers() { - var existingUserSession *models.Session - existingUserSession, ok = models.GetSessionByUserId(existingUser.GetId()) + var existingUserSession models.Session + existingUserSession, ok = sessions.GetByUserId(existingUser.GetId()) wss.SendOrStoreMessage( existingUserSession, "ChannelChatMessage", diff --git a/server/internal/routes/game/chat/sendWhisper.go b/server/internal/routes/game/chat/sendWhisper.go index f4e036cd..1434d241 100644 --- a/server/internal/routes/game/chat/sendWhisper.go +++ b/server/internal/routes/game/chat/sendWhisper.go @@ -1,9 +1,7 @@ package chat import ( - "encoding/json" "net/http" - "strconv" "github.com/luskaner/ageLANServer/common" i "github.com/luskaner/ageLANServer/server/internal" @@ -11,6 +9,14 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/wss" ) +type recipientIDs struct { + IDs i.Json[[]int32] `schema:"recipientIDs"` +} + +type recipientID struct { + ID int32 `schema:"recipientID"` +} + func whisperResult(w *http.ResponseWriter, gameId string, code int) { response := i.A{code} if gameId == common.GameAoM { @@ -22,42 +28,32 @@ func whisperResult(w *http.ResponseWriter, gameId string, code int) { func SendWhisper(w http.ResponseWriter, r *http.Request) { game := models.G(r) gameTitle := game.Title() - text := r.FormValue("message") - if text == "" { + var req textRequest + err := i.Bind(r, &req) + if err != nil { whisperResult(&w, gameTitle, 2) return } - var targetUserIds []int32 + var targetUserIds recipientIDs if gameTitle == common.GameAoM { - targetUserIdsStr := r.FormValue("recipientIDs") - if targetUserIdsStr == "" { - whisperResult(&w, gameTitle, 2) - return - } - err := json.Unmarshal([]byte(targetUserIdsStr), &targetUserIds) - if err != nil { + if err := i.Bind(r, &targetUserIds); err != nil { whisperResult(&w, gameTitle, 2) return } } else { - targetUserIdStr := r.FormValue("recipientID") - if targetUserIdStr == "" { - whisperResult(&w, gameTitle, 2) - return - } - targetUserId, err := strconv.ParseInt(targetUserIdStr, 10, 32) - if err != nil { + var recpId recipientID + if err = i.Bind(r, &recpId); err != nil { whisperResult(&w, gameTitle, 2) return } - targetUserIds = append(targetUserIds, int32(targetUserId)) + targetUserIds.IDs.Data = append(targetUserIds.IDs.Data, recpId.ID) } users := game.Users() - receivers := make([]*models.MainUser, len(targetUserIds)) + receivers := make([]models.User, len(targetUserIds.IDs.Data)) var ok bool - for j, profileId := range targetUserIds { + for j, profileId := range targetUserIds.IDs.Data { receivers[j], ok = users.GetUserById(profileId) if !ok { whisperResult(&w, gameTitle, 2) @@ -77,14 +73,15 @@ func SendWhisper(w http.ResponseWriter, r *http.Request) { message, i.A{ "", - text, + req.Message, }..., ) } - message = append(message, text) - var receiverSession *models.Session + message = append(message, req.Message) + sessions := game.Sessions() + var receiverSession models.Session for _, receiver := range receivers { - receiverSession, ok = models.GetSessionByUserId(receiver.GetId()) + receiverSession, ok = sessions.GetByUserId(receiver.GetId()) if !ok { continue } diff --git a/server/internal/routes/game/cloud/getFileURL.go b/server/internal/routes/game/cloud/getFileURL.go index 2e142c71..ffcab071 100644 --- a/server/internal/routes/game/cloud/getFileURL.go +++ b/server/internal/routes/game/cloud/getFileURL.go @@ -1,7 +1,6 @@ package cloud import ( - "encoding/json" "fmt" "net/http" @@ -10,19 +9,22 @@ import ( "github.com/luskaner/ageLANServer/server/internal/models" ) +type getFileURLRequest struct { + Names i.Json[[]string] `json:"names"` +} + func GetFileURL(w http.ResponseWriter, r *http.Request) { - namesStr := r.URL.Query().Get("names") - var names []string - err := json.Unmarshal([]byte(namesStr), &names) + var req getFileURLRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2, i.A{nil}}) return } game := models.G(r) - descriptions := make(i.A, len(names)) + descriptions := make(i.A, len(req.Names.Data)) gameTitle := game.Title() - for j, name := range names { - fileData, ok := game.Resources().CloudFiles.Value[name] + for j, name := range req.Names.Data { + fileData, ok := game.Resources().CloudFiles().Value[name] if !ok { i.JSON(&w, i.A{2, i.A{nil}}) return diff --git a/server/internal/routes/game/cloud/getTempCredentials.go b/server/internal/routes/game/cloud/getTempCredentials.go index 29238b2f..8f1fc772 100644 --- a/server/internal/routes/game/cloud/getTempCredentials.go +++ b/server/internal/routes/game/cloud/getTempCredentials.go @@ -15,15 +15,15 @@ func GetTempCredentials(w http.ResponseWriter, r *http.Request) { fullKey := r.URL.Query().Get("key") key := strings.TrimPrefix(fullKey, "/cloudfiles/") game := models.G(r) - cloudfiles := game.Resources().CloudFiles - cred := cloudfiles.Credentials.CreateCredentials(key) - t := cred.GetExpiry() + cloudfiles := game.Resources().CloudFiles() + cred := models.CreateCredential(cloudfiles.Credentials, &key) + t := cred.Expiry() tUnix := t.Unix() for _, file := range cloudfiles.Value { if file.Key == key { se := url.QueryEscape(t.Format(time.RFC3339)) sv := url.QueryEscape(file.Version) - i.JSON(&w, i.A{0, tUnix, fmt.Sprintf("title=%s&sig=%s&se=%s&sv=%s&sp=r&sr=b", game.Title(), url.QueryEscape(cred.GetSignature()), se, sv), fullKey}) + i.JSON(&w, i.A{0, tUnix, fmt.Sprintf("title=%s&sig=%s&se=%s&sv=%s&sp=r&sr=b", game.Title(), url.QueryEscape(cred.Id()), se, sv), fullKey}) return } } diff --git a/server/internal/routes/game/invitation/cancelInvitation.go b/server/internal/routes/game/invitation/cancelInvitation.go index fcdc229d..f1e66ea6 100644 --- a/server/internal/routes/game/invitation/cancelInvitation.go +++ b/server/internal/routes/game/invitation/cancelInvitation.go @@ -27,7 +27,7 @@ func CancelInvitation(w http.ResponseWriter, r *http.Request) { return } peers := adv.GetPeers() - var peer *models.MainPeer + var peer models.Peer sess := models.SessionOrPanic(r) u, ok := game.Users().GetUserById(sess.GetUserId()) if !ok { @@ -39,7 +39,7 @@ func CancelInvitation(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{2}) return } - var invitee *models.MainUser + var invitee models.User invitee, ok = game.Users().GetUserById(q.UserId) if !ok { i.JSON(&w, i.A{2}) @@ -49,8 +49,8 @@ func CancelInvitation(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{0}) return } - var inviteeSession *models.Session - inviteeSession, ok = models.GetSessionByUserId(invitee.GetId()) + var inviteeSession models.Session + inviteeSession, ok = game.Sessions().GetByUserId(invitee.GetId()) if ok { wss.SendOrStoreMessage( inviteeSession, diff --git a/server/internal/routes/game/invitation/extendInvitation.go b/server/internal/routes/game/invitation/extendInvitation.go index 1e3a0ecb..6102eb03 100644 --- a/server/internal/routes/game/invitation/extendInvitation.go +++ b/server/internal/routes/game/invitation/extendInvitation.go @@ -34,7 +34,7 @@ func ExtendInvitation(w http.ResponseWriter, r *http.Request) { return } peers := adv.GetPeers() - var peer *models.MainPeer + var peer models.Peer sess := models.SessionOrPanic(r) u, ok := game.Users().GetUserById(sess.GetUserId()) if !ok { @@ -46,7 +46,7 @@ func ExtendInvitation(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{2}) return } - var invitee *models.MainUser + var invitee models.User invitee, ok = game.Users().GetUserById(q.UserId) if !ok { i.JSON(&w, i.A{2}) @@ -56,8 +56,8 @@ func ExtendInvitation(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{0}) return } - var inviteeSession *models.Session - inviteeSession, ok = models.GetSessionByUserId(invitee.GetId()) + var inviteeSession models.Session + inviteeSession, ok = game.Sessions().GetByUserId(invitee.GetId()) if ok { wss.SendOrStoreMessage( inviteeSession, diff --git a/server/internal/routes/game/invitation/replyToInvitation.go b/server/internal/routes/game/invitation/replyToInvitation.go index efead5ec..f540118f 100644 --- a/server/internal/routes/game/invitation/replyToInvitation.go +++ b/server/internal/routes/game/invitation/replyToInvitation.go @@ -27,14 +27,14 @@ func ReplyToInvitation(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{2}) return } - var inviter *models.MainUser + var inviter models.User inviter, ok = game.Users().GetUserById(q.InviterId) if !ok { i.JSON(&w, i.A{2}) return } peers := adv.GetPeers() - var peer *models.MainPeer + var peer models.Peer sess := models.SessionOrPanic(r) u, ok := game.Users().GetUserById(sess.GetUserId()) if !ok { @@ -50,8 +50,8 @@ func ReplyToInvitation(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{0}) return } - var inviterSession *models.Session - inviterSession, ok = models.GetSessionByUserId(inviter.GetId()) + var inviterSession models.Session + inviterSession, ok = game.Sessions().GetByUserId(inviter.GetId()) if ok { var acceptStr string if q.Accept { diff --git a/server/internal/routes/game/item/getInventoryByProfileIDs.go b/server/internal/routes/game/item/getInventoryByProfileIDs.go index fafb9be1..8aed0218 100644 --- a/server/internal/routes/game/item/getInventoryByProfileIDs.go +++ b/server/internal/routes/game/item/getInventoryByProfileIDs.go @@ -1,29 +1,31 @@ package item import ( - "encoding/json" "net/http" "strconv" i "github.com/luskaner/ageLANServer/server/internal" ) +type getInventoryByProfileIDsRequest struct { + ProfileIDs i.Json[[]int32] `json:"profileIDs"` +} + func GetInventoryByProfileIDs(w http.ResponseWriter, r *http.Request) { - profileIdsStr := r.URL.Query().Get("profileIDs") - var profileIds []int32 - err := json.Unmarshal([]byte(profileIdsStr), &profileIds) + var req getInventoryByProfileIDsRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2}) return } - initialData := make(i.A, len(profileIds)) - finalData := make(i.A, len(profileIds)) + initialData := make(i.A, len(req.ProfileIDs.Data)) + finalData := make(i.A, len(req.ProfileIDs.Data)) finalDataArr := i.A{ // What this mean? i.A{1, 0, 0, 0, 10000, 0, 0, 0, 1}, i.A{2, 0, 1, 0, 10000, 0, 1, 1, 0}, } - for j, profileId := range profileIds { + for j, profileId := range req.ProfileIDs.Data { profileIdStr := strconv.Itoa(int(profileId)) initialData[j] = i.A{ profileIdStr, diff --git a/server/internal/routes/game/leaderboard/age3/setAvatarStatValues.go b/server/internal/routes/game/leaderboard/age3/setAvatarStatValues.go deleted file mode 100644 index 6b019dfc..00000000 --- a/server/internal/routes/game/leaderboard/age3/setAvatarStatValues.go +++ /dev/null @@ -1,51 +0,0 @@ -package age3 - -import ( - "encoding/json" - "net/http" - "time" - - i "github.com/luskaner/ageLANServer/server/internal" - "github.com/luskaner/ageLANServer/server/internal/models" -) - -func SetAvatarStatValues(w http.ResponseWriter, r *http.Request) { - avatarStatIdsStr := r.URL.Query().Get("avatarStat_ids") - if len(avatarStatIdsStr) < 1 { - i.JSON(&w, i.A{2, i.A{}}) - return - } - var avatarStatIds []int32 - err := json.Unmarshal([]byte(avatarStatIdsStr), &avatarStatIds) - if err != nil { - i.JSON(&w, i.A{2, i.A{}}) - return - } - valuesStr := r.URL.Query().Get("values") - if len(avatarStatIdsStr) < 1 { - i.JSON(&w, i.A{2, i.A{}, i.A{}}) - return - } - var values []int32 - err = json.Unmarshal([]byte(valuesStr), &values) - if err != nil { - i.JSON(&w, i.A{2, i.A{}, i.A{}}) - return - } - if len(avatarStatIds) != len(values) { - i.JSON(&w, i.A{2, i.A{}, i.A{}}) - return - } - response := make([]i.A, len(avatarStatIds)) - users := models.G(r).Users() - - for j := 0; j < len(response); j++ { - u, ok := users.GetUserByStatId(avatarStatIds[j]) - if !ok { - i.JSON(&w, i.A{2, i.A{}, i.A{}}) - return - } - response[j] = i.A{avatarStatIds[j], u.GetId(), values[j], "", time.Now().UTC().Unix()} - } - i.JSON(&w, i.A{0, i.A{0}, response}) -} diff --git a/server/internal/routes/game/leaderboard/getAvailableLeaderboards.go b/server/internal/routes/game/leaderboard/getAvailableLeaderboards.go index 67469148..cd8c3598 100644 --- a/server/internal/routes/game/leaderboard/getAvailableLeaderboards.go +++ b/server/internal/routes/game/leaderboard/getAvailableLeaderboards.go @@ -8,5 +8,5 @@ import ( ) func GetAvailableLeaderboards(w http.ResponseWriter, r *http.Request) { - i.JSON(&w, models.G(r).Resources().ArrayFiles["leaderboards.json"]) + i.JSON(&w, models.G(r).Resources().ArrayFiles()["leaderboards.json"]) } diff --git a/server/internal/routes/game/leaderboard/setAvatarStatValues.go b/server/internal/routes/game/leaderboard/setAvatarStatValues.go index 95fce669..182b5a39 100644 --- a/server/internal/routes/game/leaderboard/setAvatarStatValues.go +++ b/server/internal/routes/game/leaderboard/setAvatarStatValues.go @@ -3,9 +3,82 @@ package leaderboard import ( "net/http" + mapset "github.com/deckarep/golang-set/v2" + "github.com/luskaner/ageLANServer/common" i "github.com/luskaner/ageLANServer/server/internal" + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/routes/wss" ) -func SetAvatarStatValues(w http.ResponseWriter, _ *http.Request) { +var fixedAvatarNames = map[string]mapset.Set[string]{ + common.GameAoM: mapset.NewSet[string]("STAT_GAUNTLET_REWARD_FAVOUR", "STAT_GAUNTLET_REWARD_XP"), +} + +type setAvatarStatValuesRequest struct { + AvatarStatIds i.Json[[]int32] `schema:"avatarStat_ids"` + Values i.Json[[]int64] `schema:"values"` + // TODO: Implement when we know what it means + UpdateTypes i.Json[[]int32] `schema:"updateTypes"` +} + +func SetAvatarStatValues(w http.ResponseWriter, r *http.Request) { + var req setAvatarStatValuesRequest + if err := i.Bind(r, &req); err != nil || len(req.Values.Data) < 1 || len(req.AvatarStatIds.Data) != len(req.Values.Data) || len(req.UpdateTypes.Data) != len(req.Values.Data) { + i.JSON(&w, i.A{2}) + } + game := models.G(r) + users := game.Users() + sess := models.SessionOrPanic(r) + u, ok := users.GetUserById(sess.GetUserId()) + if !ok { + i.JSON(&w, i.A{2}) + return + } + var encodedAvatarStats i.A + var currentGameFixedAvatarNames mapset.Set[string] + avatarStatDefinitions := game.LeaderboardDefinitions().AvatarStatDefinitions() + data := u.GetAvatarStats() + fixedAvatarIds := mapset.NewThreadUnsafeSet[int32]() + if currentGameFixedAvatarNames, ok = fixedAvatarNames[game.Title()]; ok { + for name := range currentGameFixedAvatarNames.Iter() { + if id, ok := avatarStatDefinitions.GetIdByName(name); ok { + fixedAvatarIds.Add(id) + } + } + } + values := make(map[int32]int64) + for j := 0; j < len(req.AvatarStatIds.Data); j++ { + avatarStatId := req.AvatarStatIds.Data[j] + if fixedAvatarIds.ContainsOne(avatarStatId) { + continue + } + values[avatarStatId] = req.Values.Data[j] + } + if len(values) > 0 { + _ = data.WithReadWrite(func(avatarStats *models.AvatarStats) error { + for avatarStatId, value := range values { + var avatarStat models.AvatarStat + if avatarStat, ok = avatarStats.GetStat(avatarStatId); ok { + avatarStat.SetValue(value) + } else { + avatarStat = models.NewAvatarStat(avatarStatId, value) + avatarStats.AddStat(avatarStat) + } + encodedAvatarStats = append(encodedAvatarStats, avatarStat.Encode(u.GetProfileId())) + } + return nil + }) + } + if len(encodedAvatarStats) > 0 { + // TODO: Do others need to be notified? + // TODO: Does client support multiple AvatarStats in one message? + wss.SendOrStoreMessage( + sess, + "AvatarStatsUpdatedMessage", + i.A{ + encodedAvatarStats, + }, + ) + } i.JSON(&w, i.A{0}) } diff --git a/server/internal/routes/game/leaderboard/shared/leaderboard.go b/server/internal/routes/game/leaderboard/shared/leaderboard.go index 2a6e5a01..68af96a4 100644 --- a/server/internal/routes/game/leaderboard/shared/leaderboard.go +++ b/server/internal/routes/game/leaderboard/shared/leaderboard.go @@ -14,13 +14,12 @@ func GetStatGroups(r *http.Request, idsQuery string, isProfileId bool, includeEx if err != nil { return nil } - message := i.A{0, i.A{}, i.A{}, i.A{}} game := models.G(r) users := game.Users() clientLibVersion := models.SessionOrPanic(r).GetClientLibVersion() for _, id := range ids { - var u *models.MainUser + var u models.User var ok bool if isProfileId { u, ok = users.GetUserById(id) diff --git a/server/internal/routes/game/login/logout.go b/server/internal/routes/game/login/logout.go index 42210c9c..4cc26389 100644 --- a/server/internal/routes/game/login/logout.go +++ b/server/internal/routes/game/login/logout.go @@ -27,21 +27,22 @@ func Logout(w http.ResponseWriter, r *http.Request) { game.Advertisements().UnsafeRemovePeer(adv.GetId(), u.GetId()) }) } + sessions := game.Sessions() for channelId, channel := range game.ChatChannels().Iter() { if channel.RemoveUser(u) { - chat.NotifyLeaveChannel(users, u, channelId, sess.GetClientLibVersion()) + chat.NotifyLeaveChannel(sessions, users, u, channelId, sess.GetClientLibVersion()) // AoE3 only takes into account the first notify in a readSession return // so delay each message by 100ms so they go in different responses // otherwise, it would appear as it left the first channel only time.Sleep(100 * time.Millisecond) } } - relationship.ChangePresence(sess.GetClientLibVersion(), users, u, 0) + relationship.ChangePresence(sess.GetClientLibVersion(), sessions, users, u, 0) if game.Title() == common.GameAoE3 || game.Title() == common.GameAoM { profileInfo := u.GetProfileInfo(false, sess.GetClientLibVersion()) for user := range users.GetUserIds() { if user != u.GetId() { - currentSess, currentOk := models.GetSessionByUserId(user) + currentSess, currentOk := sessions.GetByUserId(user) if currentOk { wss.SendOrStoreMessage( currentSess, @@ -52,6 +53,6 @@ func Logout(w http.ResponseWriter, r *http.Request) { } } } - sess.Delete() + sessions.Delete(sess.Id()) i.JSON(&w, i.A{0}) } diff --git a/server/internal/routes/game/login/platformlogin.go b/server/internal/routes/game/login/platformlogin.go index b4658b91..67dd22cf 100644 --- a/server/internal/routes/game/login/platformlogin.go +++ b/server/internal/routes/game/login/platformlogin.go @@ -28,28 +28,35 @@ func Platformlogin(w http.ResponseWriter, r *http.Request) { i.JSON(&w, i.A{2, "", 0, t, i.A{}, i.A{}, 0, 0, nil, nil, i.A{}, i.A{}, 0, i.A{}}) return } - var t2 int64 - var t3 int64 - i.WithRng(func(rand *i.RandReader) { - t2 = t - rand.Int64N(3600*2-3600+1) + 3600 - t3 = t - rand.Int64N(3600*2-3600+1) + 3600 - }) game := models.G(r) title := game.Title() users := game.Users() - u := users.GetOrCreateUser(title, r.RemoteAddr, req.MacAddress, req.AccountType == "XBOXLIVE", req.PlatformUserId, req.Alias) - sess, ok := models.GetSessionByUserId(u.GetId()) + sessions := game.Sessions() + var avatarStatDefinitions models.AvatarStatDefinitions = nil + if title != common.GameAoE1 { + avatarStatDefinitions = game.LeaderboardDefinitions().AvatarStatDefinitions() + } + u := users.GetOrCreateUser( + title, + avatarStatDefinitions, + r.RemoteAddr, + req.MacAddress, + req.AccountType == "XBOXLIVE", + req.PlatformUserId, + req.Alias, + ) + sess, ok := sessions.GetByUserId(u.GetId()) if ok { - sess.Delete() + sessions.Delete(sess.Id()) } - sessionId := models.CreateSession(req.GameId, u.GetId(), req.ClientLibVersion) - sess, _ = models.GetSessionById(sessionId) - relationship.ChangePresence(req.ClientLibVersion, users, u, 1) + sessionId := sessions.Create(u.GetId(), req.ClientLibVersion) + sess, _ = sessions.GetById(sessionId) + relationship.ChangePresence(req.ClientLibVersion, sessions, users, u, 1) profileInfo := u.GetProfileInfo(false, req.ClientLibVersion) if title == common.GameAoE3 || title == common.GameAoM { for user := range users.GetUserIds() { if user != u.GetId() { - currentSess, currentOk := models.GetSessionByUserId(user) + currentSess, currentOk := sessions.GetByUserId(user) if currentOk { wss.SendOrStoreMessage( currentSess, @@ -65,50 +72,16 @@ func Platformlogin(w http.ResponseWriter, r *http.Request) { if title == common.GameAoE2 { extraProfileInfoList = append(extraProfileInfoList, u.GetExtraProfileInfo(req.ClientLibVersion)) } - var unknownProfileInfoList i.A - switch title { - case common.GameAoE2: - unknownProfileInfoList = i.A{ - i.A{2, profileId, 0, "", t2}, - i.A{39, profileId, 671, "", t2}, - i.A{41, profileId, 191, "", t2}, - i.A{42, profileId, 480, "", t2}, - i.A{44, profileId, 0, "", t2}, - i.A{45, profileId, 0, "", t2}, - i.A{46, profileId, 0, "", t2}, - i.A{47, profileId, 0, "", t2}, - i.A{48, profileId, 0, "", t2}, - i.A{50, profileId, 0, "", t2}, - i.A{60, profileId, 1, "", t2}, - i.A{142, profileId, 1, "", t3}, - i.A{171, profileId, 1, "", t2}, - i.A{172, profileId, 4, "", t2}, - i.A{173, profileId, 1, "", t2}, - } - case common.GameAoE3, common.GameAoM: - unknownProfileInfoList = i.A{ - i.A{291, u.GetId(), 16, "", t2}, - } - default: - unknownProfileInfoList = i.A{} - } - if title == common.GameAoM { - unknownProfileInfoList = append( - unknownProfileInfoList, - // 10k of favour stash (the maximum) - i.A{117, u.GetId(), 10_000, "", t2}, - ) - } battleServers := game.BattleServers() servers := battleServers.Encode(r) if len(servers) == 0 { server := battleServers.NewBattleServer("") - server.IPv4 = "127.0.0.1" - server.BsPort = 27012 - server.WebSocketPort = 27112 + server.SetIPv4("127.0.0.1") + server.SetBsPort(27012) + server.SetWebSocketPort(27112) if title != common.GameAoE1 { - server.Name = "localhost" - server.OutOfBandPort = 27212 + server.SetName("localhost") + server.SetOutOfBandPort(27212) } servers = append(servers, server.EncodeLogin(r)) } @@ -133,15 +106,18 @@ func Platformlogin(w http.ResponseWriter, r *http.Request) { 0, nil, } + var avatarStats i.A if title == common.GameAoE1 { response = append(response, i.A{}) + } else { + avatarStats = u.EncodeAvatarStats() } allProfileInfo := i.A{ 0, profileInfo, relationship.Relationships(title, req.ClientLibVersion, users, u), extraProfileInfoList, - unknownProfileInfoList, + avatarStats, nil, i.A{}, nil, @@ -154,7 +130,7 @@ func Platformlogin(w http.ResponseWriter, r *http.Request) { allProfileInfo = append(allProfileInfo, -1) } response = append(response, - game.Resources().LoginData, + game.Resources().LoginData(), allProfileInfo, i.A{}, 0, diff --git a/server/internal/routes/game/login/readSession.go b/server/internal/routes/game/login/readSession.go index f2cb52fd..656ad139 100644 --- a/server/internal/routes/game/login/readSession.go +++ b/server/internal/routes/game/login/readSession.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "strconv" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" @@ -19,18 +18,18 @@ func returnError(w http.ResponseWriter) { returnData(w, 0, []i.A{}) } +type readSessionRequest struct { + Ack uint `schema:"ack"` +} + func ReadSession(w http.ResponseWriter, r *http.Request) { - ackId := r.FormValue("ack") - if ackId == "" { - returnError(w) - return - } - ackIdUint, err := strconv.ParseUint(ackId, 10, 32) + var req readSessionRequest + err := i.Bind(r, &req) if err != nil { returnError(w) return } sess := models.SessionOrPanic(r) - messageId, messages := sess.WaitForMessages(uint(ackIdUint)) + messageId, messages := sess.WaitForMessages(req.Ack) returnData(w, messageId, messages) } diff --git a/server/internal/routes/game/party/peerAdd.go b/server/internal/routes/game/party/peerAdd.go index 8c8b204e..6fc051b1 100644 --- a/server/internal/routes/game/party/peerAdd.go +++ b/server/internal/routes/game/party/peerAdd.go @@ -29,9 +29,9 @@ func PeerAdd(w http.ResponseWriter, r *http.Request) { if hostId := adv.GetHostId(); hostId != currentUserId { return } - users := make([]*models.MainUser, length) + users := make([]models.User, length) for j := 0; j < length; j++ { - var u *models.MainUser + var u models.User u, ok = gameUsers.GetUserById(profileIds[j]) if !ok { return diff --git a/server/internal/routes/game/party/peerUpdate.go b/server/internal/routes/game/party/peerUpdate.go index 6e951e9d..b9f49026 100644 --- a/server/internal/routes/game/party/peerUpdate.go +++ b/server/internal/routes/game/party/peerUpdate.go @@ -31,14 +31,14 @@ func PeerUpdate(w http.ResponseWriter, r *http.Request) { return } advPeers := adv.GetPeers() - var peers []*models.MainPeer + var peers []models.Peer for j := 0; j < length; j++ { - var u *models.MainUser + var u models.User u, ok = gameUsers.GetUserById(profileIds[j]) if !ok { return } - var peer *models.MainPeer + var peer models.Peer peer, ok = advPeers.Load(u.GetId()) if !ok { return diff --git a/server/internal/routes/game/party/sendMatchChat.go b/server/internal/routes/game/party/sendMatchChat.go index 98ef1fb9..d6127ad2 100644 --- a/server/internal/routes/game/party/sendMatchChat.go +++ b/server/internal/routes/game/party/sendMatchChat.go @@ -1,10 +1,8 @@ package party import ( - "encoding/json" "net/http" "slices" - "strconv" "github.com/luskaner/ageLANServer/common" i "github.com/luskaner/ageLANServer/server/internal" @@ -19,6 +17,14 @@ type request struct { Message string `schema:"message"` } +type profileIds struct { + Ids i.Json[[]int32] `schema:"to_profile_ids"` +} + +type profileId struct { + Id int32 `schema:"to_profile_id"` +} + func SendMatchChat(w http.ResponseWriter, r *http.Request) { // FIXME: AoE3 duplicate messages/wrong total var req request @@ -27,26 +33,18 @@ func SendMatchChat(w http.ResponseWriter, r *http.Request) { return } - var toProfileIds []int32 + var toProfileIds profileIds game := models.G(r) if game.Title() == common.GameAoE3 { - profileIdStr := r.FormValue("to_profile_id") - if profileIdStr == "" { - i.JSON(&w, i.A{0}) - return - } - profileId, err := strconv.ParseInt(profileIdStr, 10, 32) - if err != nil { - i.JSON(&w, i.A{2}) - return - } - toProfileIds = append(toProfileIds, int32(profileId)) - } else { - err := json.Unmarshal([]byte(r.FormValue("to_profile_ids")), &toProfileIds) - if err != nil { + var toProfileId profileId + if err := i.Bind(r, &toProfileId); err != nil { i.JSON(&w, i.A{2}) return } + toProfileIds.Ids.Data = append(toProfileIds.Ids.Data, toProfileId.Id) + } else if err := i.Bind(r, &toProfileIds); err != nil { + i.JSON(&w, i.A{2}) + return } adv, ok := game.Advertisements().GetAdvertisement(req.MatchID) @@ -67,12 +65,12 @@ func SendMatchChat(w http.ResponseWriter, r *http.Request) { users := game.Users() if game.Title() == common.GameAoM { - toProfileIds = slices.DeleteFunc(toProfileIds, func(id int32) bool { return id == currentUserId }) + toProfileIds.Ids.Data = slices.DeleteFunc(toProfileIds.Ids.Data, func(id int32) bool { return id == currentUserId }) } - receivers := make([]*models.MainUser, len(toProfileIds)) + receivers := make([]models.User, len(toProfileIds.Ids.Data)) - for j, profileId := range toProfileIds { - receivers[j], ok = users.GetUserById(profileId) + for j, profId := range toProfileIds.Ids.Data { + receivers[j], ok = users.GetUserById(profId) if !ok { i.JSON(&w, i.A{2}) return @@ -93,9 +91,10 @@ func SendMatchChat(w http.ResponseWriter, r *http.Request) { ) messageEncoded := message.Encode() - var receiverSession *models.Session + sessions := game.Sessions() + var receiverSession models.Session for _, receiver := range receivers { - receiverSession, ok = models.GetSessionByUserId(receiver.GetId()) + receiverSession, ok = sessions.GetByUserId(receiver.GetId()) if !ok { continue } diff --git a/server/internal/routes/game/party/shared/peer.go b/server/internal/routes/game/party/shared/peer.go index 78962e9d..81524ec1 100644 --- a/server/internal/routes/game/party/shared/peer.go +++ b/server/internal/routes/game/party/shared/peer.go @@ -1,41 +1,33 @@ package shared import ( - "encoding/json" "net/http" - "strconv" + + i "github.com/luskaner/ageLANServer/server/internal" ) +type peerRequest struct { + MatchID int32 `schema:"match_id"` + ProfileIDs i.Json[[]int32] `schema:"profile_ids"` + RaceIDs i.Json[[]int32] `schema:"race_ids"` + TeamIDs i.Json[[]int32] `schema:"teamIDs"` +} + func ParseParameters(r *http.Request) (parseError bool, advId int32, length int, profileIds []int32, raceIds []int32, teamIds []int32) { - profileIdsStr := r.PostFormValue("profile_ids") - err := json.Unmarshal([]byte(profileIdsStr), &profileIds) - if err != nil { - parseError = true - return - } - raceIdsStr := r.PostFormValue("race_ids") - err = json.Unmarshal([]byte(raceIdsStr), &raceIds) - if err != nil { - parseError = true - return - } - teamIdsStr := r.PostFormValue("teamIDs") - err = json.Unmarshal([]byte(teamIdsStr), &teamIds) + var req peerRequest + err := i.Bind(r, &req) if err != nil { parseError = true return } + profileIds = req.ProfileIDs.Data + raceIds = req.RaceIDs.Data + teamIds = req.TeamIDs.Data if min(len(profileIds), len(raceIds), len(teamIds)) != max(len(profileIds), len(raceIds), len(teamIds)) { parseError = true return } - advIdStr := r.PostFormValue("match_id") - advIdInt64, err := strconv.ParseInt(advIdStr, 10, 32) - if err != nil { - parseError = true - return - } - advId = int32(advIdInt64) + advId = req.MatchID length = len(profileIds) return } diff --git a/server/internal/routes/game/party/updateHost.go b/server/internal/routes/game/party/updateHost.go index 903cd956..0e99b786 100644 --- a/server/internal/routes/game/party/updateHost.go +++ b/server/internal/routes/game/party/updateHost.go @@ -2,20 +2,23 @@ package party import ( "net/http" - "strconv" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" ) +type advRequest struct { + MatchID int32 `schema:"match_id"` +} + func UpdateHost(w http.ResponseWriter, r *http.Request) { - advStr := r.PostFormValue("match_id") - advId, err := strconv.ParseInt(advStr, 10, 32) + var req advRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2}) return } - _, ok := models.G(r).Advertisements().GetAdvertisement(int32(advId)) + _, ok := models.G(r).Advertisements().GetAdvertisement(req.MatchID) if !ok { i.JSON(&w, i.A{2}) } else { diff --git a/server/internal/routes/game/relationship/addfriend.go b/server/internal/routes/game/relationship/addfriend.go index 2db1def0..5e26ec54 100644 --- a/server/internal/routes/game/relationship/addfriend.go +++ b/server/internal/routes/game/relationship/addfriend.go @@ -2,22 +2,24 @@ package relationship import ( "net/http" - "strconv" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" ) -func Addfriend(w http.ResponseWriter, r *http.Request) { - profileIdStr := r.PostFormValue("targetProfileID") +type friendRequest struct { + TargetProfileID int32 `schema:"targetProfileID"` +} - profileId, err := strconv.ParseInt(profileIdStr, 10, 32) +func Addfriend(w http.ResponseWriter, r *http.Request) { + var req friendRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2, i.A{}}) return } game := models.G(r) - u, ok := game.Users().GetUserById(int32(profileId)) + u, ok := game.Users().GetUserById(req.TargetProfileID) if !ok { i.JSON(&w, i.A{2, i.A{}}) return diff --git a/server/internal/routes/game/relationship/getRelationships.go b/server/internal/routes/game/relationship/getRelationships.go index 69fe588c..a3613e1b 100644 --- a/server/internal/routes/game/relationship/getRelationships.go +++ b/server/internal/routes/game/relationship/getRelationships.go @@ -21,8 +21,8 @@ func relationshipResponse(errorCode int, friends []i.A, lastConnection []i.A) i. } } -func Relationships(gameTitle string, clientLibVersion uint16, users *models.MainUsers, user *models.MainUser) i.A { - profileInfo := users.GetProfileInfo(true, func(u *models.MainUser) bool { +func Relationships(gameTitle string, clientLibVersion uint16, users models.Users, user models.User) i.A { + profileInfo := users.GetProfileInfo(true, func(u models.User) bool { return u != user && u.GetPresence() > 0 }, clientLibVersion) friends := profileInfo diff --git a/server/internal/routes/game/relationship/ignore.go b/server/internal/routes/game/relationship/ignore.go index a07c468b..17d92d5b 100644 --- a/server/internal/routes/game/relationship/ignore.go +++ b/server/internal/routes/game/relationship/ignore.go @@ -2,22 +2,20 @@ package relationship import ( "net/http" - "strconv" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" ) func Ignore(w http.ResponseWriter, r *http.Request) { - profileIdStr := r.PostFormValue("targetProfileID") - - profileId, err := strconv.ParseInt(profileIdStr, 10, 32) + var req friendRequest + err := i.Bind(r, &req) if err != nil { i.JSON(&w, i.A{2, i.A{}, i.A{}}) return } game := models.G(r) - u, ok := game.Users().GetUserById(int32(profileId)) + u, ok := game.Users().GetUserById(req.TargetProfileID) if !ok { i.JSON(&w, i.A{2, i.A{}, i.A{}}) return diff --git a/server/internal/routes/game/relationship/setPresence.go b/server/internal/routes/game/relationship/setPresence.go index be5ba9a5..ef1cbf47 100644 --- a/server/internal/routes/game/relationship/setPresence.go +++ b/server/internal/routes/game/relationship/setPresence.go @@ -2,18 +2,17 @@ package relationship import ( "net/http" - "strconv" i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" "github.com/luskaner/ageLANServer/server/internal/routes/wss" ) -func ChangePresence(clientLibVersion uint16, users *models.MainUsers, user *models.MainUser, presence int32) { +func ChangePresence(clientLibVersion uint16, sessions models.Sessions, users models.Users, user models.User, presence int32) { user.SetPresence(presence) profileInfo := i.A{user.GetProfileInfo(true, clientLibVersion)} for u := range users.GetUserIds() { - sess, ok := models.GetSessionByUserId(u) + sess, ok := sessions.GetByUserId(u) if ok { wss.SendOrStoreMessage( sess, @@ -24,14 +23,13 @@ func ChangePresence(clientLibVersion uint16, users *models.MainUsers, user *mode } } +type setPresenceRequest struct { + PresenceId int32 `schema:"presence_id"` +} + func SetPresence(w http.ResponseWriter, r *http.Request) { - presenceId := r.PostFormValue("presence_id") - if presenceId == "" { - i.JSON(&w, i.A{2}) - return - } - presence, err := strconv.ParseInt(presenceId, 10, 8) - if err != nil { + var req setPresenceRequest + if err := i.Bind(r, &req); err != nil { i.JSON(&w, i.A{2}) return } @@ -40,7 +38,7 @@ func SetPresence(w http.ResponseWriter, r *http.Request) { users := game.Users() u, ok := users.GetUserById(sess.GetUserId()) if ok { - ChangePresence(sess.GetClientLibVersion(), users, u, int32(presence)) + ChangePresence(sess.GetClientLibVersion(), game.Sessions(), users, u, req.PresenceId) i.JSON(&w, i.A{0}) } else { i.JSON(&w, i.A{2}) diff --git a/server/internal/routes/playfab/Catalog/GetItems.go b/server/internal/routes/playfab/Catalog/GetItems.go index 0d64884b..68a5987c 100644 --- a/server/internal/routes/playfab/Catalog/GetItems.go +++ b/server/internal/routes/playfab/Catalog/GetItems.go @@ -1,9 +1,9 @@ package Catalog import ( - "encoding/json" "net/http" + i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" "github.com/luskaner/ageLANServer/server/internal/models/athens" "github.com/luskaner/ageLANServer/server/internal/models/playfab" @@ -23,7 +23,7 @@ type getItemsResponse struct { func GetItems(w http.ResponseWriter, r *http.Request) { var req getItemsRequest - err := json.NewDecoder(r.Body).Decode(&req) + err := i.Bind(r, &req) if err != nil || len(req.Ids) == 0 { shared.RespondBadRequest(&w) return diff --git a/server/internal/routes/playfab/Client/GetPlayerCombinedInfo.go b/server/internal/routes/playfab/Client/GetPlayerCombinedInfo.go index 5f8445da..c405683a 100644 --- a/server/internal/routes/playfab/Client/GetPlayerCombinedInfo.go +++ b/server/internal/routes/playfab/Client/GetPlayerCombinedInfo.go @@ -22,11 +22,11 @@ type getPlayerCombinedInfoRequest struct { } func GetPlayerCombinedInfo(w http.ResponseWriter, r *http.Request) { - playfabId, _ := playfab.Id(playfab.Session(r)) + sess := playfab.SessionOrPanic(r) shared.RespondOK( &w, getPlayerCombinedInfoRequest{ - PlayFabId: playfabId, + PlayFabId: sess.PlayfabId(), infoResultPayload: infoResultPayload{ UserInventory: []any{}, CharacterInventories: []any{}, diff --git a/server/internal/routes/playfab/Client/GetUserReadOnlyData.go b/server/internal/routes/playfab/Client/GetUserReadOnlyData.go index 416c5218..f94c74f6 100644 --- a/server/internal/routes/playfab/Client/GetUserReadOnlyData.go +++ b/server/internal/routes/playfab/Client/GetUserReadOnlyData.go @@ -1,40 +1,19 @@ package Client import ( - "encoding/json" "net/http" "strings" - "time" + i "github.com/luskaner/ageLANServer/server/internal" + "github.com/luskaner/ageLANServer/server/internal/models/athens/user" "github.com/luskaner/ageLANServer/server/internal/models/playfab" + "github.com/luskaner/ageLANServer/server/internal/models/playfab/data" "github.com/luskaner/ageLANServer/server/internal/routes/playfab/Client/shared" ) -type punchCardProgress struct { - Holes uint8 - DateOfMostRecentHolePunch playfab.CustomTime -} - -type punchCardProgressValue struct { - playfab.Value[punchCardProgress] -} - -type missionProgress struct { - State string - RewardsAwarded string - CompletionCountEasy uint32 - CompletionCountMedium uint32 - CompletionCountHard uint32 -} - -type missionProgressValue struct { - playfab.Value[missionProgress] - name string -} - type getUserReadOnlyDataResponse struct { - DataVersion int32 - Data map[string]playfab.ValueLike + DataVersion uint32 + Data map[string]any } type getUserReadonlyDataRequest struct { @@ -43,42 +22,24 @@ type getUserReadonlyDataRequest struct { PlayFabId *string } -func getType(key string) string { - switch { - case key == "PunchCardProgress": - return "punchCardProgress" - case strings.HasPrefix(key, "Mission_"): - return "missionProgress" - default: - return "" +func nullableData[T any](val *data.Value[T]) any { + if val == nil { + return nil } + return val } -func getValue(key string) playfab.ValueLike { - switch getType(key) { - case "punchCardProgress": - return &punchCardProgressValue{ - playfab.Value[punchCardProgress]{ - Val: &punchCardProgress{ - Holes: 0, - DateOfMostRecentHolePunch: playfab.CustomTime{ - Time: time.Date(2024, 5, 2, 3, 34, 0, 0, time.UTC), - Format: time.RFC3339, - }, - }, - }, - } - case "missionProgress": - return &missionProgressValue{ - playfab.Value[missionProgress]{ - Val: &missionProgress{ - State: "Completed", - RewardsAwarded: "Hard", - CompletionCountHard: 1, - }, - }, - key, - } +func getValue(key string, userData *user.Data) any { + switch { + case key == "PunchCardProgress": + return userData.PunchCardProgress.ToValue() + case key == "CurrentGauntletProgress": + return nullableData(userData.Challenge.Progress.ToValue()) + case key == "CurrentGauntletLabyrinth": + return nullableData(userData.Challenge.Labyrinth.ToValue()) + case strings.HasPrefix(key, "Mission_Season0_"): + storyMission := userData.StoryMissions[key] + return storyMission.ToValue() default: return nil } @@ -86,21 +47,28 @@ func getValue(key string) playfab.ValueLike { func GetUserReadOnlyData(w http.ResponseWriter, r *http.Request) { var req getUserReadonlyDataRequest - err := json.NewDecoder(r.Body).Decode(&req) + err := i.Bind(r, &req) if err != nil || len(req.Keys) == 0 { shared.RespondBadRequest(&w) return } + sess := playfab.SessionOrPanic(r) + u := sess.User() + d := u.(*user.User).PlayfabData var response getUserReadOnlyDataResponse - response.Data = make(map[string]playfab.ValueLike) - for _, key := range req.Keys { - if val := getValue(key); val != nil { - if err = val.Prepare(); err != nil { - shared.RespondBadRequest(&w) - return + _ = d.WithReadOnly(func(d *user.Data) error { + response = getUserReadOnlyDataResponse{ + DataVersion: d.DataVersion, + Data: make(map[string]any), + } + if req.IfChangedFromDataVersion == nil || *req.IfChangedFromDataVersion < d.DataVersion { + for _, key := range req.Keys { + if val := getValue(key, d); val != nil { + response.Data[key] = val + } } - response.Data[key] = val } - } + return nil + }) shared.RespondOK(&w, response) } diff --git a/server/internal/routes/playfab/Client/LoginWithSteam.go b/server/internal/routes/playfab/Client/LoginWithSteam.go index 486c4a91..d42930c7 100644 --- a/server/internal/routes/playfab/Client/LoginWithSteam.go +++ b/server/internal/routes/playfab/Client/LoginWithSteam.go @@ -1,10 +1,14 @@ package Client import ( + "io" "net/http" "time" "github.com/google/uuid" + i "github.com/luskaner/ageLANServer/server/internal" + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/athens" "github.com/luskaner/ageLANServer/server/internal/models/playfab" "github.com/luskaner/ageLANServer/server/internal/routes/playfab/Client/shared" ) @@ -32,6 +36,10 @@ type settingsForUserResponse struct { GatherFocusInfo bool } +type loginWithSteamRequest struct { + SteamTicket string +} + type loginWithSteamResponse struct { SessionTicket string PlayFabId string @@ -42,10 +50,26 @@ type loginWithSteamResponse struct { treatmentAssignmentResponse `json:"TreatmentAssignment"` } -func LoginWithSteam(w http.ResponseWriter, _ *http.Request) { +func LoginWithSteam(w http.ResponseWriter, r *http.Request) { + var req loginWithSteamRequest + err := i.Bind(r, &req) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(r.Body) + if err != nil { + shared.RespondBadRequest(&w) + return + } + var steamId uint64 + steamId, err = playfab.ParseSteamIDHex(req.SteamTicket) + if err != nil { + shared.RespondBadRequest(&w) + return + } now := time.Now().UTC() - entityToken := uuid.NewString() - id := playfab.AddSession(entityToken) + game := models.Gg[*athens.Game](r) + sessions := game.PlayfabSessions + id := sessions.Create(game.Users(), steamId) shared.RespondOK( &w, loginWithSteamResponse{ @@ -59,7 +83,7 @@ func LoginWithSteam(w http.ResponseWriter, _ *http.Request) { }, LastLoginTime: shared.FormatDate(time.Date(2025, 11, 12, 3, 34, 0, 0, time.UTC)), entityTokenResponse: entityTokenResponse{ - EntityToken: entityToken, + EntityToken: id, TokenExpiration: shared.FormatDate(now.AddDate(0, 0, 1)), entityResponse: entityResponse{ Id: uuid.NewString(), diff --git a/server/internal/routes/playfab/Client/UpdateUserTitleDisplayName.go b/server/internal/routes/playfab/Client/UpdateUserTitleDisplayName.go index 57b40678..dfb9ac72 100644 --- a/server/internal/routes/playfab/Client/UpdateUserTitleDisplayName.go +++ b/server/internal/routes/playfab/Client/UpdateUserTitleDisplayName.go @@ -1,9 +1,9 @@ package Client import ( - "encoding/json" "net/http" + i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/routes/playfab/Client/shared" ) @@ -17,7 +17,7 @@ type updateUserTitleDisplayNameResponse struct { func UpdateUserTitleDisplayName(w http.ResponseWriter, r *http.Request) { var req updateUserTitleDisplayNameRequest - err := json.NewDecoder(r.Body).Decode(&req) + err := i.Bind(r, &req) if err != nil { shared.RespondBadRequest(&w) return diff --git a/server/internal/routes/playfab/CloudScript/ExecuteFunction.go b/server/internal/routes/playfab/CloudScript/ExecuteFunction.go index 86212866..0ef2cfeb 100644 --- a/server/internal/routes/playfab/CloudScript/ExecuteFunction.go +++ b/server/internal/routes/playfab/CloudScript/ExecuteFunction.go @@ -2,69 +2,68 @@ package CloudScript import ( "encoding/json" - "io" "net/http" "time" - "github.com/luskaner/ageLANServer/server/internal/models/athens/playfab/cloudScriptFunction" + i "github.com/luskaner/ageLANServer/server/internal" + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/athens/routes/playfab/cloudScriptFunction" + "github.com/luskaner/ageLANServer/server/internal/models/playfab" "github.com/luskaner/ageLANServer/server/internal/routes/playfab/Client/shared" ) -type executeFunctionRequestInitial struct { +type executeFunctionRequest struct { CustomTags struct{} Entity *struct{} FunctionName string GeneratePlayStreamEvent *struct{} -} - -type executeFunctionRequestEnd[T any] struct { - FunctionParameter T + FunctionParameter json.RawMessage } type executeFunctionResponse struct { ExecutionTimeMilliseconds int64 FunctionName string - FunctionResult json.RawMessage + FunctionResult json.RawMessage `json:",omitempty"` FunctionResultSize uint32 } func ExecuteFunction(w http.ResponseWriter, r *http.Request) { - var req executeFunctionRequestInitial - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - shared.RespondBadRequest(&w) - return - } - err = json.Unmarshal(bodyBytes, &req) + var req executeFunctionRequest + err := i.Bind(r, &req) if err != nil || req.FunctionName == "" { shared.RespondBadRequest(&w) return } if fn, ok := cloudScriptFunction.Store[req.FunctionName]; ok { - var reqPar executeFunctionRequestEnd[cloudScriptFunction.AwardMissionRewardsParameters] - err = json.Unmarshal(bodyBytes, &reqPar) - if err != nil || req.FunctionName == "" { + reqPar := fn.NewParameters() + err = json.Unmarshal(req.FunctionParameter, &reqPar) + if err != nil { shared.RespondBadRequest(&w) return } + u := playfab.SessionOrPanic(r).User() + game := models.G(r) t := time.Now() - result := fn.Run(reqPar.FunctionParameter) + result := fn.Run(game, u, reqPar) duration := time.Since(t).Milliseconds() - var resultBytes []byte - resultBytes, err = json.Marshal(result) + var tmpResultBytes []byte + tmpResultBytes, err = json.Marshal(result) if err != nil { shared.RespondBadRequest(&w) return } - shared.RespondOK( - &w, - &executeFunctionResponse{ - ExecutionTimeMilliseconds: duration, - FunctionName: req.FunctionName, - FunctionResult: resultBytes, - FunctionResultSize: uint32(len(resultBytes)), - }, - ) + var resultSize uint32 + var resultBytes []byte + if string(tmpResultBytes) != "null" { + resultBytes = tmpResultBytes + resultSize = uint32(len(resultBytes)) + } + shared.RespondOK(&w, &executeFunctionResponse{ + ExecutionTimeMilliseconds: duration, + FunctionName: req.FunctionName, + FunctionResultSize: resultSize, + FunctionResult: resultBytes, + }) } else { shared.RespondBadRequest(&w) return diff --git a/server/internal/routes/playfab/Inventory/GetInventoryItems.go b/server/internal/routes/playfab/Inventory/GetInventoryItems.go index a159fc7d..97a5b301 100644 --- a/server/internal/routes/playfab/Inventory/GetInventoryItems.go +++ b/server/internal/routes/playfab/Inventory/GetInventoryItems.go @@ -1,10 +1,10 @@ package Inventory import ( - "encoding/json" "net/http" "strconv" + i "github.com/luskaner/ageLANServer/server/internal" "github.com/luskaner/ageLANServer/server/internal/models" "github.com/luskaner/ageLANServer/server/internal/models/athens" "github.com/luskaner/ageLANServer/server/internal/models/playfab" @@ -27,7 +27,7 @@ type getInventoryItemsResponse struct { func GetInventoryItems(w http.ResponseWriter, r *http.Request) { var req getInventoryItemsRequest - err := json.NewDecoder(r.Body).Decode(&req) + err := i.Bind(r, &req) if err != nil { shared.RespondBadRequest(&w) return diff --git a/server/internal/routes/router/game.go b/server/internal/routes/router/game.go index 4309daba..9ecbdd6d 100644 --- a/server/internal/routes/router/game.go +++ b/server/internal/routes/router/game.go @@ -17,7 +17,6 @@ import ( "github.com/luskaner/ageLANServer/server/internal/routes/game/invitation" "github.com/luskaner/ageLANServer/server/internal/routes/game/item" "github.com/luskaner/ageLANServer/server/internal/routes/game/leaderboard" - "github.com/luskaner/ageLANServer/server/internal/routes/game/leaderboard/age3" "github.com/luskaner/ageLANServer/server/internal/routes/game/login" "github.com/luskaner/ageLANServer/server/internal/routes/game/news" "github.com/luskaner/ageLANServer/server/internal/routes/game/party" @@ -109,13 +108,7 @@ func (g *Game) InitializeRoutes(gameId string, _ http.Handler) http.Handler { } leaderboardGroup := gameGroup.Subgroup("/leaderboard") leaderboardGroup.HandleFunc("POST", "/applyOfflineUpdates", leaderboard.ApplyOfflineUpdates) - var setStatValues func(http.ResponseWriter, *http.Request) - if gameId == common.GameAoE3 { - setStatValues = age3.SetAvatarStatValues - } else { - setStatValues = leaderboard.SetAvatarStatValues - } - leaderboardGroup.HandleFunc("POST", "/setAvatarStatValues", setStatValues) + leaderboardGroup.HandleFunc("POST", "/setAvatarStatValues", leaderboard.SetAvatarStatValues) automatch2Group := gameGroup.Subgroup("/automatch2") automatch2Group.HandleFunc("GET", "/getAutomatchMap", Automatch2.GetAutomatchMap) diff --git a/server/internal/routes/router/playfabapiMiddleware.go b/server/internal/routes/router/playfabapiMiddleware.go index 1aba20d7..7ffdd7e6 100644 --- a/server/internal/routes/router/playfabapiMiddleware.go +++ b/server/internal/routes/router/playfabapiMiddleware.go @@ -1,9 +1,12 @@ package router import ( + "context" "net/http" "strings" + "github.com/luskaner/ageLANServer/server/internal/models" + "github.com/luskaner/ageLANServer/server/internal/models/athens" "github.com/luskaner/ageLANServer/server/internal/models/playfab" "github.com/luskaner/ageLANServer/server/internal/routes/playfab/Client/shared" ) @@ -17,22 +20,27 @@ var playAnonymousPaths = map[string]bool{ func PlayfabMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !playAnonymousPaths[r.URL.Path] && !strings.HasPrefix(r.URL.Path, playfab.StaticSuffix) { - entityToken := playfab.Session(r) - var exists bool + var sess *playfab.SessionData + entityToken := r.Header.Get("X-Entitytoken") if entityToken != "" { - _, exists = playfab.Id(entityToken) - } - if !exists { - shared.RespondError( - &w, - 401, - "Unauthorized", - 401, - "Invalid X-EntityToken header", - "", - ) - return + var exists bool + sessions := models.Gg[*athens.Game](r).PlayfabSessions + if sess, exists = sessions.GetById(entityToken); exists { + sessions.ResetExpiry(sess.EntityToken()) + ctx := context.WithValue(r.Context(), "session", sess) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } } + shared.RespondError( + &w, + 401, + "Unauthorized", + 401, + "Invalid X-EntityToken header", + "", + ) + return } next.ServeHTTP(w, r) }) diff --git a/server/internal/routes/router/sessionMiddleware.go b/server/internal/routes/router/sessionMiddleware.go index ce740ba2..f9c8d628 100644 --- a/server/internal/routes/router/sessionMiddleware.go +++ b/server/internal/routes/router/sessionMiddleware.go @@ -25,12 +25,13 @@ func SessionMiddleware(next http.Handler) http.Handler { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - sess, ok := models.GetSessionById(sessionID) + sessions := models.G(r).Sessions() + sess, ok := sessions.GetById(sessionID) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - sess.ResetExpiryTimer() + sessions.ResetExpiry(sess.Id()) ctx := context.WithValue(r.Context(), "session", sess) next.ServeHTTP(w, r.WithContext(ctx)) } else { diff --git a/server/internal/routes/wss/wss.go b/server/internal/routes/wss/wss.go index dcb08e1e..4f643e3e 100644 --- a/server/internal/routes/wss/wss.go +++ b/server/internal/routes/wss/wss.go @@ -194,24 +194,24 @@ func closeConn(conn *connectionWrapper, closeCode int, text string) { _ = conn.Close() } -func parseMessage(message i.H, currentSession *models.Session) (uint32, *models.Session) { - var sess *models.Session +func parseMessage(sessions models.Sessions, message i.H, currentSession models.Session) (uint32, models.Session) { + var sess models.Session sess = nil op := uint32(message["operation"].(float64)) if op == 0 { sessionToken, ok := message["sessionToken"] if ok { - sess, ok = models.GetSessionById(sessionToken.(string)) + sess, ok = sessions.GetById(sessionToken.(string)) if ok { return 0, sess - } else { - return 0, nil } + + return 0, nil } } if currentSession != nil { var ok bool - sess, ok = models.GetSessionById(currentSession.GetId()) + sess, ok = sessions.GetById(currentSession.Id()) if !ok { return 0, nil } @@ -224,6 +224,7 @@ func Handle(w http.ResponseWriter, r *http.Request) { if err != nil { return } + sessions := models.G(r).Sessions() connWrapper := &connectionWrapper{ connLock: &sync.RWMutex{}, writeLock: &sync.Mutex{}, @@ -258,13 +259,13 @@ func Handle(w http.ResponseWriter, r *http.Request) { return } - _, sess := parseMessage(loginMsg, nil) + _, sess := parseMessage(sessions, loginMsg, nil) if sess == nil { closeConn(connWrapper, websocket.CloseNormalClosure, "Invalid login message data") return } - sessionToken := sess.GetId() + sessionToken := sess.Id() connections.Store( sessionToken, @@ -272,7 +273,7 @@ func Handle(w http.ResponseWriter, r *http.Request) { nil, ) - sess.ResetExpiryTimer() + sessions.ResetExpiry(sess.Id()) connWrapper.SetPingHandler(func(message string) error { var pingErr error @@ -280,15 +281,15 @@ func Handle(w http.ResponseWriter, r *http.Request) { if pingErr == nil { pingErr = connWrapper.SetReadDeadline(time.Now().Add(time.Minute)) if pingErr == nil { - sess.ResetExpiryTimer() + sessions.ResetExpiry(sess.Id()) } } else if errors.Is(pingErr, websocket.ErrCloseSent) { return nil - } else { - var e net.Error - if errors.As(pingErr, &e) && e.Temporary() { - return nil - } + } + + var e net.Error + if errors.As(pingErr, &e) && e.Temporary() { + return nil } return pingErr }) @@ -305,19 +306,19 @@ func Handle(w http.ResponseWriter, r *http.Request) { if err != nil { break } - op, sess = parseMessage(msg, sess) + op, sess = parseMessage(sessions, msg, sess) if op == 0 { if sess == nil { break } - if sessId := sess.GetId(); sessId != sessionToken { + if sessId := sess.Id(); sessId != sessionToken { connections.StoreAndDelete(sessId, connWrapper, sessionToken) sessionToken = sessId } - } else if _, ok := models.GetSessionById(sessionToken); !ok { + } else if _, ok := sessions.GetById(sessionToken); !ok { break } - sess.ResetExpiryTimer() + sessions.ResetExpiry(sess.Id()) } } @@ -339,10 +340,10 @@ func sendMessage(sessionId string, message i.A) bool { return true } -func SendOrStoreMessage(session *models.Session, action string, message i.A) { +func SendOrStoreMessage(session models.Session, action string, message i.A) { finalMessage := i.A{0, action, session.GetUserId(), message} - go func(session *models.Session, finalMessage i.A) { - if ok := sendMessage(session.GetId(), finalMessage); !ok { + go func(session models.Session, finalMessage i.A) { + if ok := sendMessage(session.Id(), finalMessage); !ok { session.AddMessage(finalMessage) } }(session, finalMessage) diff --git a/server/internal/runtime.go b/server/internal/runtime.go index c897db62..ab0fab25 100644 --- a/server/internal/runtime.go +++ b/server/internal/runtime.go @@ -7,5 +7,6 @@ import ( var Id uuid.UUID var AnnounceMessageData map[string]common.AnnounceMessageData002 +var GeneratePlatformUserId bool type AnnounceMessageDataLatest = common.AnnounceMessageData002 diff --git a/server/resources/responses/athens/playfab/public-production/2/gauntlet.json b/server/resources/responses/athens/playfab/public-production/2/gauntlet.json new file mode 100644 index 00000000..4c3a1dfa --- /dev/null +++ b/server/resources/responses/athens/playfab/public-production/2/gauntlet.json @@ -0,0 +1,215 @@ +{ + "LabyrinthConfigs": [ + { + "ForGauntletDifficulties": [ + "Standard", + "Moderate" + ], + "ColumnConfigs": [ + { + "MissionPool": "Pool1" + }, + { + "MissionPool": "Pool2" + }, + { + "MissionPool": "Pool2" + }, + { + "MissionPool": "Pool2" + }, + { + "MissionPool": "Pool3" + }, + { + "MissionPool": "Pool3" + }, + { + "MissionPool": "Pool3" + }, + { + "MissionPool": "Pool4" + }, + { + "MissionPool": "Pool4" + } + ], + "BossMissionPool": "Pool5" + }, + { + "ForGauntletDifficulties": [ + "Hard" + ], + "ColumnConfigs": [ + { + "MissionPool": "Pool6" + }, + { + "MissionPool": "Pool2" + }, + { + "MissionPool": "Pool2" + }, + { + "MissionPool": "Pool2" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool5" + }, + { + "MissionPool": "Pool5" + } + ], + "BossMissionPool": "Pool9" + }, + { + "ForGauntletDifficulties": [ + "Titan" + ], + "ColumnConfigs": [ + { + "MissionPool": "Pool6" + }, + { + "MissionPool": "Pool7" + }, + { + "MissionPool": "Pool7" + }, + { + "MissionPool": "Pool7" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool9" + }, + { + "MissionPool": "Pool9" + } + ], + "BossMissionPool": "Pool9" + }, + { + "ForGauntletDifficulties": [ + "Extreme", + "Legendary" + ], + "ColumnConfigs": [ + { + "MissionPool": "Pool6" + }, + { + "MissionPool": "Pool7" + }, + { + "MissionPool": "Pool7" + }, + { + "MissionPool": "Pool7" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool8" + }, + { + "MissionPool": "Pool9" + }, + { + "MissionPool": "Pool9" + } + ], + "BossMissionPool": "Pool10" + } + ], + "Rewards": { + "ExcludeFromRegularRewards": [ + { + "EffectName": "HouseVillSpawn", + "KnownRarities": [ + 3 + ] + }, + { + "EffectName": "ArkantosStart", + "KnownRarities": [] + }, + { + "EffectName": "GiantSpawn", + "KnownRarities": [] + }, + { + "EffectName": "NidhoggSpawn", + "KnownRarities": [ + 3 + ] + }, + { + "EffectName": "HeroBoarStart", + "KnownRarities": [] + }, + { + "EffectName": "LegendKastorStart", + "KnownRarities": [] + }, + { + "EffectName": "KronosUnique", + "KnownRarities": [ + 0 + ] + }, + { + "EffectName": "GaiaUnique", + "KnownRarities": [ + 0 + ] + }, + { + "EffectName": "AndromedaRefundMythFavor", + "KnownRarities": [] + }, + { + "EffectName": "AndromedaCheaperFortress", + "KnownRarities": [] + }, + { + "EffectName": "AIServantKronosStart", + "KnownRarities": [] + }, + { + "EffectName": "AIFollowerLokiStart", + "KnownRarities": [] + }, + { + "EffectName": "AIChampionFreyrStart", + "KnownRarities": [] + }, + { + "EffectName": "BossNidhoggSpawn", + "KnownRarities": [] + } + ], + "PreferredFinalRewards": [] + } +} \ No newline at end of file diff --git a/server/resources/responses/athens/playfab/public-production/2/gauntlet_mission_pools.json b/server/resources/responses/athens/playfab/public-production/2/gauntlet_mission_pools.json new file mode 100644 index 00000000..a78a0897 --- /dev/null +++ b/server/resources/responses/athens/playfab/public-production/2/gauntlet_mission_pools.json @@ -0,0 +1,34584 @@ +[ + { + "Name": "Pool1", + "Missions": [ + { + "Id": "nomad0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "ghost_lake1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "arena2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "savannah3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "giza4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "giza5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "peach_blossom_land6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "islands7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "alfheim8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "ghost_lake9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "silk_road10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "mirkwood11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "bamboo_grove12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "mediterranean13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "muspellheim14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "mirkwood15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "highland16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "blue_lagoon17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "midgard18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "air19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "great_wall20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "ironwood21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "nile_shallows22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "oasis23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "kerlaugar24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "muspellheim25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "silk_road26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "gold_rush27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "megalopolis28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "jotunheim29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "yellow_river30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "yellow_river", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/yellow_river-sqr.png" + }, + { + "Id": "tundra31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "acropolis32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "nile_shallows33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "sea_of_worms34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "oasis35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "mount_olympus36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "islands37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "erebus38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "tundra39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "air40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "alfheim41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "tundra42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "elysium43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "anatolia44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "qinghai_lake45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "archipelago46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "qinghai_lake47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "arena48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "steppe49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + } + ] + }, + { + "Name": "Pool2", + "Missions": [ + { + "Id": "yellow_river0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "yellow_river", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/yellow_river-sqr.png" + }, + { + "Id": "kerlaugar1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "valley_of_kings2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "river_styx3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "blue_lagoon4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "anatolia5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "river_styx6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "great_wall7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "steppe8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "nile_shallows9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "alfheim10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "savannah11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "river_nile12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "team_migration13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "megalopolis14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "marsh15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "steppe16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "air17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "silk_road18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "gold_rush19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "giza20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "ironwood21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "great_wall22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "islands23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "mediterranean24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "highland25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "valley_of_kings26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "jotunheim27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "acropolis28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "Anatolia29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "erebus30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "tundra31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "nomad32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "bamboo_grove33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "archipelago34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "kerlaugar35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "oasis36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "acropolis37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "river_nile38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "arena39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "bamboo_grove40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "sea_of_worms41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "sea_of_worms42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "oasis43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "vinlandsaga44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "midgard45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "mirage46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirage", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirage-sqr.png" + }, + { + "Id": "giza47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "elysium48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "nomad49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + } + ] + }, + { + "Name": "Pool3", + "Missions": [ + { + "Id": "tundra0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "archipelago1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "qinghai_lake2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "tundra3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "gold_rush4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "valley_of_kings5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "nomad6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "erebus7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "alfheim8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "kerlaugar9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "kerlaugar10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "midgard11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "anatolia12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "ironwood13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "jotunheim14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "black_sea15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "black_sea", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/black_sea-sqr.png" + }, + { + "Id": "jotunheim16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "steppe17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "steppe18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "tundra19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "river_nile20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "mirkwood21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "watering_hole22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + }, + { + "Id": "acropolis23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "blue_lagoon24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "ghost_lake25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "peach_blossom_land26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "qinghai_lake27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "islands28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "mirkwood29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "steppe30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "nile_shallows31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD9", + "ArenaEffectName": "CelestialRemnantsD9", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "oasis32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "giza33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "mediterranean34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "bamboo_grove35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "erebus36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "acropolis37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "megalopolis38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "blue_lagoon39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "peach_blossom_land40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "elysium41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "marsh42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "mount_olympus43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "savannah44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "qinghai_lake45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "air46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "river_nile47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "nile_shallows48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "team_migration49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + } + ] + }, + { + "Name": "Pool4", + "Missions": [ + { + "Id": "air0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "ironwood1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "steppe2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "archipelago3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "anatolia4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "islands5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "anatolia6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "blue_lagoon7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "team_migration8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "watering_hole9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + }, + { + "Id": "midgard10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "blue_lagoon11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "erebus12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "giza13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD8", + "ArenaEffectName": "CelestialRemnantsD8", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "kerlaugar14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "sea_of_worms15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "arena16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "gold_rush17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "archipelago18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "midgard19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "marsh20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "highland21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "jotunheim22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "silk_road23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "mount_olympus24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "black_sea25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "black_sea", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/black_sea-sqr.png" + }, + { + "Id": "ghost_lake26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "megalopolis27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 3, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 3, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "steppe28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "team_migration29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "acropolis30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "valley_of_kings31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "nomad32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "mediterranean33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "giza34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "marsh35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "tundra36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "elysium37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "bamboo_grove38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD9", + "ArenaEffectName": "CelestialRemnantsD9", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 3, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 3, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "vinlandsaga39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 3, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "highland40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "jotunheim41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD8", + "ArenaEffectName": "CelestialRemnantsD8", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 3, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 3, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "acropolis42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 3, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "erebus43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "islands44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "river_styx45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 3, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 3, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "peach_blossom_land46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 3, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 3, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "ghost_lake47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 3, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "river_styx48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 3, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 3, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "watering_hole49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 3, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + } + ] + }, + { + "Name": "Pool5", + "Missions": [ + { + "Id": "mirage0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirage", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirage-sqr.png" + }, + { + "Id": "peach_blossom_land1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "yellow_river2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "yellow_river", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/yellow_river-sqr.png" + }, + { + "Id": "team_migration3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "ironwood4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "nomad5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "mirkwood6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "arena7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "vinlandsaga8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "river_styx9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "jotunheim10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "midgard11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "blue_lagoon12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "oasis13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "midgard14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "alfheim15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "great_wall16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "black_sea17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "black_sea", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/black_sea-sqr.png" + }, + { + "Id": "giza18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "acropolis19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "tundra20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "bamboo_grove21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "megalopolis22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "nile_shallows23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "kerlaugar24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD9", + "ArenaEffectName": "CelestialRemnantsD9", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "mount_olympus25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "marsh26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "air27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "gold_rush28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "sea_of_worms29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "savannah30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "mediterranean31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "mediterranean32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "defender.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "defender.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "mirkwood33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "river_nile34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "elysium35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "anatolia36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "defender.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "defender.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "silk_road37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "oasis38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "valley_of_kings39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "megalopolis40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "watering_hole41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "humanoid.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + }, + { + "Id": "muspellheim42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "supporter.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "supporter.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "archipelago43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "supporter.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "supporter.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "steppe44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "balanced.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "balanced.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "qinghai_lake45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "islands46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "supporter.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "supporter.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "ghost_lake47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD8", + "ArenaEffectName": "CelestialRemnantsD8", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "defender.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "defender.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "highland48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "attacker.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "erebus49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "balanced.personality" + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "balanced.personality" + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + } + ] + }, + { + "Name": "Pool6", + "Missions": [ + { + "Id": "nomad0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "ghost_lake1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "arena2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "savannah3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "giza4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "giza5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "peach_blossom_land6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "islands7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "alfheim8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "ghost_lake9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "silk_road10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "mirkwood11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "bamboo_grove12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "mediterranean13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "muspellheim14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "mirkwood15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "highland16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "blue_lagoon17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "midgard18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "air19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "great_wall20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "ironwood21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "nile_shallows22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "oasis23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "kerlaugar24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "muspellheim25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "silk_road26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "gold_rush27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "megalopolis28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "jotunheim29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "yellow_river30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "yellow_river", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/yellow_river-sqr.png" + }, + { + "Id": "midgard31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "acropolis32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "nile_shallows33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "sea_of_worms34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "oasis35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "mount_olympus36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "islands37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "erebus38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "tundra39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "air40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "alfheim41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "tundra42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "elysium43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "anatolia44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "qinghai_lake45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "archipelago46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "qinghai_lake47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "arena48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "steppe49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + } + ] + }, + { + "Name": "Pool7", + "Missions": [ + { + "Id": "yellow_river0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "yellow_river", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/yellow_river-sqr.png" + }, + { + "Id": "kerlaugar1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "valley_of_kings2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "river_styx3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "blue_lagoon4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "anatolia5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "river_styx6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "great_wall7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "air8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "nile_shallows9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "alfheim10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "savannah11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "river_nile12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "team_migration13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "megalopolis14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "marsh15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "steppe16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "air17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "silk_road18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "gold_rush19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "giza20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "ironwood21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "great_wall22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "islands23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "mediterranean24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "highland25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "valley_of_kings26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "jotunheim27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "acropolis28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "nomad29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "erebus30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "tundra31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "nomad32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "bamboo_grove33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "archipelago34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "kerlaugar35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "oasis36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "acropolis37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "river_nile38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "arena39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "bamboo_grove40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "sea_of_worms41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "sea_of_worms42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "oasis43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "vinlandsaga44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "midgard45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "mirage46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirage", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirage-sqr.png" + }, + { + "Id": "giza47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "elysium48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "nomad49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + } + ] + }, + { + "Name": "Pool8", + "Missions": [ + { + "Id": "tundra0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "archipelago1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "qinghai_lake2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "tundra3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "gold_rush4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "valley_of_kings5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "nomad6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "mirkwood7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "alfheim8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "kerlaugar9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "kerlaugar10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "midgard11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "anatolia12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "ironwood13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "jotunheim14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "black_sea15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "black_sea", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/black_sea-sqr.png" + }, + { + "Id": "jotunheim16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "river_styx17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "steppe18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "vinlandsaga19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "river_nile20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "mirkwood21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "watering_hole22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + }, + { + "Id": "acropolis23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "blue_lagoon24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "ghost_lake25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "peach_blossom_land26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "passive.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "passive.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "muspellheim27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "overwhelmer.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "islands28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "mirkwood29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "steppe30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "nile_shallows31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD9", + "ArenaEffectName": "CelestialRemnantsD9", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "oasis32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "giza33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Nuwa", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "mediterranean34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "conqueror.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "bamboo_grove35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + }, + { + "Civ": "Shennong", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": -1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "erebus36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "acropolis37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "megalopolis38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "blue_lagoon39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "peach_blossom_land40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "elysium41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "marsh42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "mount_olympus43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "savannah44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "qinghai_lake45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "air46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "river_nile47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "nile_shallows48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "team_migration49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + } + ] + }, + { + "Name": "Pool9", + "Missions": [ + { + "Id": "air0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "ironwood1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "steppe2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "archipelago3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "anatolia4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "islands5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "anatolia6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "blue_lagoon7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "team_migration8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "watering_hole9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + }, + { + "Id": "midgard10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "blue_lagoon11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "erebus12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "giza13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD8", + "ArenaEffectName": "CelestialRemnantsD8", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "kerlaugar14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "sea_of_worms15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "arena16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "gold_rush17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "archipelago18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "midgard19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "marsh20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "highland21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "jotunheim22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "silk_road23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "mount_olympus24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "black_sea25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "black_sea", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/black_sea-sqr.png" + }, + { + "Id": "ghost_lake26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 0, + "Personality": "balanced.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "megalopolis27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "steppe28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "team_migration29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "acropolis30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "valley_of_kings31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 0, + "Personality": "defender.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "nomad32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 0, + "Personality": "attacker.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "mediterranean33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 0, + "Personality": "supporter.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "kerlaugar34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 0, + "Personality": "mythical.personality", + "DifficultyOffset": -1 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 1 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "marsh35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "tundra36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "elysium37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "bamboo_grove38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD9", + "ArenaEffectName": "CelestialRemnantsD9", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "vinlandsaga39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "highland40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "passive.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "jotunheim41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD8", + "ArenaEffectName": "CelestialRemnantsD8", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "acropolis42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "erebus43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + }, + { + "Id": "islands44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 2, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "river_styx45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "peach_blossom_land46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "passive.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "ghost_lake47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 2, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 2, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "river_styx48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 2, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 2, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "watering_hole49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 2, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 2, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + } + ] + }, + { + "Name": "Pool10", + "Missions": [ + { + "Id": "mirage0", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirage", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirage-sqr.png" + }, + { + "Id": "peach_blossom_land1", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "peach_blossom_land", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/peach_blossom_land-sqr.png" + }, + { + "Id": "yellow_river2", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "yellow_river", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/yellow_river-sqr.png" + }, + { + "Id": "team_migration3", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "team_migration", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/team_migration-sqr.png" + }, + { + "Id": "ironwood4", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ironwood", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ironwood-sqr.png" + }, + { + "Id": "nomad5", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nomad", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nomad-sqr.png" + }, + { + "Id": "mirkwood6", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "arena7", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "arena", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/arena-sqr.png" + }, + { + "Id": "vinlandsaga8", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "river_styx9", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_styx", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_styx-sqr.png" + }, + { + "Id": "jotunheim10", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "jotunheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/jotunheim-sqr.png" + }, + { + "Id": "midgard11", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "blue_lagoon12", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "blue_lagoon", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/blue_lagoon-sqr.png" + }, + { + "Id": "vinlandsaga13", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "vinlandsaga", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/vinlandsaga-sqr.png" + }, + { + "Id": "midgard14", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "midgard", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Odin", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/midgard-sqr.png" + }, + { + "Id": "alfheim15", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "alfheim", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/alfheim-sqr.png" + }, + { + "Id": "great_wall16", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "great_wall", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/great_wall-sqr.png" + }, + { + "Id": "black_sea17", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "black_sea", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/black_sea-sqr.png" + }, + { + "Id": "giza18", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "giza", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/giza-sqr.png" + }, + { + "Id": "acropolis19", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "acropolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/acropolis-sqr.png" + }, + { + "Id": "tundra20", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "tundra", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/tundra-sqr.png" + }, + { + "Id": "bamboo_grove21", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "bamboo_grove", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD2", + "ArenaEffectName": "SeasonsD2", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Kronos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/bamboo_grove-sqr.png" + }, + { + "Id": "megalopolis22", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD5", + "ArenaEffectName": "CelestialRemnantsD5", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "nile_shallows23", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "nile_shallows", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/nile_shallows-sqr.png" + }, + { + "Id": "kerlaugar24", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "kerlaugar", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD9", + "ArenaEffectName": "CelestialRemnantsD9", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "SeasonsD3", + "ArenaEffectName": "SeasonsD3", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/kerlaugar-sqr.png" + }, + { + "Id": "mount_olympus25", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mount_olympus", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD7", + "ArenaEffectName": "CelestialRemnantsD7", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mount_olympus-sqr.png" + }, + { + "Id": "marsh26", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "marsh", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/marsh-sqr.png" + }, + { + "Id": "air27", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "air", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD4", + "ArenaEffectName": "SeasonsD4", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/air-sqr.png" + }, + { + "Id": "gold_rush28", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "gold_rush", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD5", + "ArenaEffectName": "SeasonsD5", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Thor", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/gold_rush-sqr.png" + }, + { + "Id": "sea_of_worms29", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "sea_of_worms", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/sea_of_worms-sqr.png" + }, + { + "Id": "savannah30", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "savannah", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/savannah-sqr.png" + }, + { + "Id": "mediterranean31", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD0", + "ArenaEffectName": "CelestialRemnantsD0", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Oranos", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "mediterranean32", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mediterranean", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD6", + "ArenaEffectName": "SeasonsD6", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mediterranean-sqr.png" + }, + { + "Id": "mirkwood33", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "mirkwood", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD7", + "ArenaEffectName": "SeasonsD7", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/mirkwood-sqr.png" + }, + { + "Id": "river_nile34", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "river_nile", + "Size": "Normal", + "VictoryCondition": "Standard", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD1", + "ArenaEffectName": "CelestialRemnantsD1", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/river_nile-sqr.png" + }, + { + "Id": "elysium35", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "elysium", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD8", + "ArenaEffectName": "SeasonsD8", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Set", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/elysium-sqr.png" + }, + { + "Id": "anatolia36", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "anatolia", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Thor", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/anatolia-sqr.png" + }, + { + "Id": "silk_road37", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "silk_road", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/silk_road-sqr.png" + }, + { + "Id": "oasis38", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "oasis", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/oasis-sqr.png" + }, + { + "Id": "valley_of_kings39", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "valley_of_kings", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Ra", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Zeus", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/valley_of_kings-sqr.png" + }, + { + "Id": "megalopolis40", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "megalopolis", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "SeasonsD9", + "ArenaEffectName": "SeasonsD9", + "Title": "STR_AOTG_RULE_SEASONS_PHASE2", + "Description": "STR_AOTG_RULE_SEASONS_PHASE2_DESC", + "OwnerIcon": "resources/norse/static_color/major_gods/loki_icon_round.png", + "OwnerPortrait": "resources/norse/static_color/major_gods/loki_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/megalopolis-sqr.png" + }, + { + "Id": "watering_hole41", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "watering_hole", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD2", + "ArenaEffectName": "CelestialRemnantsD2", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + } + ], + "Opponents": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/watering_hole-sqr.png" + }, + { + "Id": "muspellheim42", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "muspellheim", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "PrometheusMinorWT", + "ArenaEffectName": "PrometheusMinorWT", + "Title": "STR_AOTG_RULE_PROMETHEUS_MINOR", + "Description": "STR_AOTG_RULE_PROMETHEUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/prometheus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD0", + "ArenaEffectName": "SeasonsD0", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Shennong", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/muspellheim-sqr.png" + }, + { + "Id": "archipelago43", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "archipelago", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Set", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/archipelago-sqr.png" + }, + { + "Id": "steppe44", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "steppe", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "CelestialRemnantsD3", + "ArenaEffectName": "CelestialRemnantsD3", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Loki", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/steppe-sqr.png" + }, + { + "Id": "qinghai_lake45", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "qinghai_lake", + "Size": "Normal", + "VictoryCondition": "SuddenDeath", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "SeasonsD1", + "ArenaEffectName": "SeasonsD1", + "Title": "STR_AOTG_RULE_SEASONS", + "Description": "STR_AOTG_RULE_SEASONS_DESC", + "OwnerIcon": "resources/norse_freyr/static_color/major_gods/freyr_icon_round.png", + "OwnerPortrait": "resources/norse_freyr/static_color/major_gods/freyr_icon.png", + "Visualization": "Right" + }, + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Oranos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Hades", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/qinghai_lake-sqr.png" + }, + { + "Id": "islands46", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "islands", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "AphroditeMinorWT", + "ArenaEffectName": "AphroditeMinorWT", + "Title": "STR_AOTG_RULE_APHRODITE_MINOR", + "Description": "STR_AOTG_RULE_APHRODITE_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/aphrodite_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD4", + "ArenaEffectName": "CelestialRemnantsD4", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Zeus", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "defender.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Odin", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Freyr", + "Team": 1, + "Personality": "mythical.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/islands-sqr.png" + }, + { + "Id": "ghost_lake47", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "ghost_lake", + "Size": "Normal", + "VictoryCondition": "KingOfTheHill", + "GameType": "Deathmatch", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "ThothMinorWT", + "ArenaEffectName": "ThothMinorWT", + "Title": "STR_AOTG_RULE_THOTH_MINOR", + "Description": "STR_AOTG_RULE_THOTH_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/thoth_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD8", + "ArenaEffectName": "CelestialRemnantsD8", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Loki", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Poseidon", + "Team": 1, + "Personality": "balanced.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "conqueror.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Nuwa", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/ghost_lake-sqr.png" + }, + { + "Id": "highland48", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "highland", + "Size": "Normal", + "VictoryCondition": "Conquest", + "GameType": "Standard", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "CelestialRemnantsD6", + "ArenaEffectName": "CelestialRemnantsD6", + "Title": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS", + "Description": "STR_CELESTIAL_RULE_ATLANTEAN_REMNANTS_DESC", + "OwnerIcon": "resources/atlantean/static_color/major_gods/gaia_icon_round.png", + "OwnerPortrait": "resources/atlantean/static_color/major_gods/gaia_icon.png", + "Visualization": "Right" + }, + { + "Id": "HelMinorWT", + "ArenaEffectName": "HelMinorWT", + "Title": "STR_AOTG_RULE_HEL_MINOR", + "Description": "STR_AOTG_RULE_HEL_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/hel_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/hel_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Gaia", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Ra", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Isis", + "Team": 1, + "Personality": "attacker.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/highland-sqr.png" + }, + { + "Id": "erebus49", + "Predecessors": [], + "PositionX": 0, + "PositionY": 0, + "Visualization": "Regular", + "Map": "erebus", + "Size": "Normal", + "VictoryCondition": "Regicide", + "GameType": "Lightning", + "MapVisibility": "Normal", + "StartingResources": "Standard", + "AllowTitans": true, + "WorldTwists": [ + { + "Id": "FreyjaMinorWT", + "ArenaEffectName": "FreyjaMinorWT", + "Title": "STR_AOTG_RULE_FREYJA_MINOR", + "Description": "STR_AOTG_RULE_FREYJA_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/freyja_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "AthenaMinorWT", + "ArenaEffectName": "AthenaMinorWT", + "Title": "STR_AOTG_RULE_ATHENA_MINOR_ALT", + "Description": "STR_AOTG_RULE_ATHENA_MINOR_ALT_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/athena_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/athena_minor_wt_icon.png", + "Visualization": "Left" + }, + { + "Id": "HorusMinorWT", + "ArenaEffectName": "HorusMinorWT", + "Title": "STR_AOTG_RULE_HORUS_MINOR", + "Description": "STR_AOTG_RULE_HORUS_MINOR_DESC", + "OwnerIcon": "resources/aotg/static_color/world_twists/horus_minor_wt_icon_round.png", + "OwnerPortrait": "resources/aotg/static_color/world_twists/horus_minor_wt_icon.png", + "Visualization": "Left" + } + ], + "Opponents": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "OpponentsFor2PlayerCoop": [ + { + "Civ": "Shennong", + "Team": 1, + "Personality": "economist.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Gaia", + "Team": 1, + "Personality": "overwhelmer.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Kronos", + "Team": 1, + "Personality": "supporter.personality", + "DifficultyOffset": 0 + }, + { + "Civ": "Fuxi", + "Team": 1, + "Personality": "humanoid.personality", + "DifficultyOffset": 0 + } + ], + "Rewards": [], + "MinimapImage": "", + "MapPreviewImage": "resources/maps/previews/erebus-sqr.png" + } + ] + } +] \ No newline at end of file diff --git a/server/resources/responses/athens/playfab/public-production/2/manifest.json b/server/resources/responses/athens/playfab/public-production/2/manifest.json index a2dadd1f..0afd39ef 100644 --- a/server/resources/responses/athens/playfab/public-production/2/manifest.json +++ b/server/resources/responses/athens/playfab/public-production/2/manifest.json @@ -7,6 +7,8 @@ } ], "DailySkirmishPlusPath": "daily_skirmish_plus.json", + "GauntletPath": "gauntlet.json", + "GauntletMissionPoolsPath": "gauntlet_mission_pools.json", "FeatureFlagsPath": "feature_flags.json", "KnownBlessingsPath": "known_blessings.json" } \ No newline at end of file diff --git a/tools/scripts/cmd/copyBattleServerManagerResources.go b/tools/scripts/cmd/copyBattleServerManagerResources.go new file mode 100644 index 00000000..0b06771b --- /dev/null +++ b/tools/scripts/cmd/copyBattleServerManagerResources.go @@ -0,0 +1,11 @@ +package main + +import ( + i "scripts/internal" + + e "github.com/luskaner/ageLANServer/common/executables" +) + +func main() { + i.CopyGameConfigs(e.BattleServerManager) +} diff --git a/tools/scripts/cmd/copyLauncherResources.go b/tools/scripts/cmd/copyLauncherResources.go new file mode 100644 index 00000000..2ea3fb17 --- /dev/null +++ b/tools/scripts/cmd/copyLauncherResources.go @@ -0,0 +1,13 @@ +package main + +import ( + i "scripts/internal" + + e "github.com/luskaner/ageLANServer/common/executables" +) + +func main() { + module := e.Launcher + i.CopyGameConfigs(module) + i.CopyMainConfig(module) +} diff --git a/tools/scripts/cmd/copyServerResources.go b/tools/scripts/cmd/copyServerResources.go new file mode 100644 index 00000000..9888d610 --- /dev/null +++ b/tools/scripts/cmd/copyServerResources.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "os" + "path/filepath" + i "scripts/internal" + + e "github.com/luskaner/ageLANServer/common/executables" +) + +func main() { + module := e.Server + src := i.ResourcePath(module) + dst := i.BuildResourcePath(module) + + err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + target := filepath.Join(dst, rel) + if info.IsDir() { + i.MkdirP(target) + return nil + } + return i.Cp(path, target) + }) + if err != nil { + log.Fatal(err) + } +} diff --git a/tools/scripts/cmd/createServerResourcesFolder.go b/tools/scripts/cmd/createServerResourcesFolder.go new file mode 100644 index 00000000..4758050a --- /dev/null +++ b/tools/scripts/cmd/createServerResourcesFolder.go @@ -0,0 +1,10 @@ +package main + +import ( + i "scripts/internal" +) + +func main() { + dst := i.BuildResourcePath("server") + i.MkdirP(dst) +} diff --git a/tools/scripts/cmd/generateGoreleaserConfig.go b/tools/scripts/cmd/generateGoreleaserConfig.go new file mode 100644 index 00000000..7f7c9c15 --- /dev/null +++ b/tools/scripts/cmd/generateGoreleaserConfig.go @@ -0,0 +1,10 @@ +package main + +import "scripts/internal/goreleaser" + +func main() { + err := goreleaser.Generate() + if err != nil { + panic(err) + } +} diff --git a/tools/scripts/go.mod b/tools/scripts/go.mod new file mode 100644 index 00000000..b87dcd66 --- /dev/null +++ b/tools/scripts/go.mod @@ -0,0 +1,72 @@ +module scripts + +go 1.25.5 + +require ( + github.com/deckarep/golang-set/v2 v2.8.0 + github.com/google/uuid v1.6.0 + github.com/goreleaser/goreleaser/v2 v2.13.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251120230642-dcccabe2cd63 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/AlekSi/pointer v1.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/caarlos0/log v0.5.4 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9 // indirect + github.com/charmbracelet/x/ansi v0.11.3 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.6.2 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/cloudflare/circl v1.6.2 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/goreleaser/chglog v0.7.4 // indirect + github.com/goreleaser/fileglob v1.4.0 // indirect + github.com/goreleaser/nfpm/v2 v2.44.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/tools/scripts/go.sum b/tools/scripts/go.sum new file mode 100644 index 00000000..38e8da54 --- /dev/null +++ b/tools/scripts/go.sum @@ -0,0 +1,93 @@ +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251120230642-dcccabe2cd63 h1:KgI+p678truaonNOQek4i+aJdWAtdpvFzz5lqHBaDeI= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/caarlos0/log v0.5.4 h1:4DJwTt8MvvRF8BM4I3j2sbmdf4DYY0HVqKpg09cAgaU= +github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8= +github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/ultraviolet v0.0.0-20251217160852-6b0c0e26fad9 h1:dsDBRP9Iyco0EjVpCsAzl8VGbxk04fP3sa80ySJSAZw= +github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/rpmpack v0.7.1 h1:YdWh1IpzOjBz60Wvdw0TU0A5NWP+JTVHA5poDqwMO2o= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/goreleaser/chglog v0.7.4 h1:3pnNt/XCrUcAOq+KC91Azlgp5CRv4GHo1nl8Aws7OzI= +github.com/goreleaser/fileglob v1.4.0 h1:Y7zcUnzQjT1gbntacGAkIIfLv+OwojxTXBFxjSFoBBs= +github.com/goreleaser/goreleaser/v2 v2.13.2 h1:kLT1bU6dmNmlfdb4vDp5UnKpPTdDX7NJq+vyFVF4FW8= +github.com/goreleaser/nfpm/v2 v2.44.1 h1:g+QNjkEx+C2Zu8dB48t9da/VfV0CWS5TMjxT8HG1APY= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/tools/scripts/internal/constants/constants.go b/tools/scripts/internal/constants/constants.go new file mode 100644 index 00000000..984efde6 --- /dev/null +++ b/tools/scripts/internal/constants/constants.go @@ -0,0 +1,7 @@ +package constants + +const BuildDir = "build" +const configPrefix = "config" +const ConfigFileName = configPrefix + ConfigExtension +const GameConfigFileName = configPrefix + ".%s" + ConfigExtension +const ConfigExtension = ".toml" diff --git a/tools/scripts/internal/copyResources.go b/tools/scripts/internal/copyResources.go new file mode 100644 index 00000000..8880ddfb --- /dev/null +++ b/tools/scripts/internal/copyResources.go @@ -0,0 +1,37 @@ +package internal + +import ( + "fmt" + "log" + "path/filepath" + c "scripts/internal/constants" + + "github.com/luskaner/ageLANServer/common" + p "github.com/luskaner/ageLANServer/common/paths" +) + +func BuildResourcePath(module string) string { + return filepath.Join(c.BuildDir, module, p.ResourcesDir) +} + +func ResourcePath(module string) string { + return filepath.Join(module, p.ResourcesDir) +} + +func CopyMainConfig(module string) { + if err := Cp(filepath.Join(ResourcePath(module), c.ConfigFileName), filepath.Join(BuildResourcePath(module), c.ConfigFileName)); err != nil { + log.Fatal(err) + } +} + +func CopyGameConfigs(module string) { + dstPath := BuildResourcePath(module) + MkdirP(dstPath) + src := filepath.Join(ResourcePath(module), fmt.Sprintf(c.GameConfigFileName, "game")) + for game := range common.SupportedGames.Iter() { + dst := filepath.Join(dstPath, fmt.Sprintf(c.GameConfigFileName, game)) + if err := Cp(src, dst); err != nil { + log.Fatal(err) + } + } +} diff --git a/tools/scripts/internal/file.go b/tools/scripts/internal/file.go new file mode 100644 index 00000000..d5849b3b --- /dev/null +++ b/tools/scripts/internal/file.go @@ -0,0 +1,35 @@ +package internal + +import ( + "io" + "log" + "os" + "path/filepath" +) + +func Cp(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer func(in *os.File) { + _ = in.Close() + }(in) + MkdirP(filepath.Dir(dst)) + out, err := os.Create(dst) + if err != nil { + return err + } + defer func(out *os.File) { + _ = out.Close() + }(out) + + _, err = io.Copy(out, in) + return err +} + +func MkdirP(path string) { + if err := os.MkdirAll(path, 0755); err != nil { + log.Fatal(err) + } +} diff --git a/tools/scripts/internal/goreleaser/archive.go b/tools/scripts/internal/goreleaser/archive.go new file mode 100644 index 00000000..22f02998 --- /dev/null +++ b/tools/scripts/internal/goreleaser/archive.go @@ -0,0 +1,543 @@ +package goreleaser + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/goreleaser/goreleaser/v2/pkg/config" + "github.com/luskaner/ageLANServer/common" +) + +func extChange(ext string) DestinationFn { + return func(source string) Renders[FileData] { + base := strings.TrimSuffix(source, filepath.Ext(source)) + if ext == "" { + return LiteralString[FileData](base) + } + return LiteralString[FileData](base + "." + ext) + } +} + +type DestinationFn func(source string) Renders[FileData] +type DestinationsFnMap = map[OperatingSystem][]DestinationFn +type SourceIgnoreFn = map[OperatingSystem]func(path string) bool +type OperatingSystemsArchs = map[OperatingSystem]mapset.Set[Architecture] + +type File struct { + source string + destination string + mode os.FileMode + os *OperatingSystem +} + +type FileData struct { + BaseOS string + SrcScriptExt string + DstScriptExt string + DstDocExt string + Game string +} + +func NewFileData(os OperatingSystem) FileData { + f := FileData{} + switch os { + case OSWindows: + f.BaseOS = "windows" + f.SrcScriptExt = "bat" + f.DstScriptExt = f.SrcScriptExt + f.DstDocExt = "txt" + case OSLinux: + f.BaseOS = "unix" + f.SrcScriptExt = "sh" + f.DstScriptExt = f.SrcScriptExt + case OSMacOS: + f.BaseOS = "unix" + f.SrcScriptExt = "sh" + f.DstScriptExt = "command" + } + return f +} + +type Archive struct { + name string + files mapset.Set[File] + targets *BinaryTargets + binaries map[string]*Binary +} + +func NewArchive(name string, targets *BinaryTargets) *Archive { + return &Archive{ + name: name, + files: mapset.NewSet[File](), + targets: targets, + binaries: make(map[string]*Binary), + } +} + +func NewMergedArchive(name string, archives ...*Archive) *Archive { + mergedOsesArchs := archives[0].targets.Clone() + for _, a := range archives[1:] { + osesToDelete := make([]OperatingSystem, 0) + for osKey, mergedArchs := range *mergedOsesArchs { + archs, ok := (*a.targets)[osKey] + if !ok { + osesToDelete = append(osesToDelete, osKey) + continue + } + archsToDelete := make([]Architecture, 0) + for archKey, mergedInstSet := range mergedArchs { + instSet, ok := archs[archKey] + if !ok { + archsToDelete = append(archsToDelete, archKey) + continue + } + if mergedInstSet.IsEmpty() && instSet.IsEmpty() { + continue + } else if mergedInstSet.IsEmpty() { + mergedArchs[archKey] = instSet.Clone() + } else if instSet.IsEmpty() { + continue + } else { + result := mergedInstSet.Intersect(instSet) + if result.IsEmpty() { + archsToDelete = append(archsToDelete, archKey) + } else { + mergedArchs[archKey] = result + } + } + } + for _, archKey := range archsToDelete { + delete(mergedArchs, archKey) + } + if len(mergedArchs) == 0 { + osesToDelete = append(osesToDelete, osKey) + } + } + for _, osKey := range osesToDelete { + delete(*mergedOsesArchs, osKey) + } + } + oses := mapset.NewSetWithSize[OperatingSystem](len(*mergedOsesArchs)) + for osKey := range *mergedOsesArchs { + oses.Add(osKey) + } + mergedArchive := NewArchive(name, mergedOsesArchs) + for _, a := range archives { + for file := range a.files.Iter() { + if file.os == nil { + mergedArchive.files.Add(file) + } else if _, exists := (*mergedOsesArchs)[*file.os]; exists { + mergedArchive.files.Add(file) + } + } + for path, binary := range a.binaries { + clonedBinary := binary.CloneForOperatingSystems(oses) + if clonedBinary != nil { + mergedArchive.binaries[path] = clonedBinary + } + } + } + return mergedArchive +} + +func (a *Archive) Name() string { + return a.name +} + +func (a *Archive) AddSrcFile(source string) { + a.files.Add(File{source: source}) +} + +func (a *Archive) AddSrcDstFile(source string, destination string) { + a.files.Add(File{source: source, destination: destination}) +} + +func (a *Archive) AddSrcDstFileWithMode(source string, destination string, mode os.FileMode) { + a.files.Add(File{source: source, destination: destination, mode: mode}) +} + +func (a *Archive) addFile(os OperatingSystem, fileMode os.FileMode, fileData FileData, source Renders[FileData], sourceIgnoreFn SourceIgnoreFn, destinationFn ...DestinationFn) { + var sourceRendered string + if sourceIgnoreFn != nil && sourceIgnoreFn[os] != nil { + if sourceRendered = source.Render(fileData); sourceIgnoreFn[os](sourceRendered) { + return + } + } else { + sourceRendered = source.Render(fileData) + } + file := File{} + file.source = filepath.ToSlash(sourceRendered) + file.destination = sourceRendered + for _, destFn := range destinationFn { + file.destination = destFn(file.destination).Render(fileData) + } + file.destination = filepath.ToSlash(file.destination) + file.os = &os + if UnixBasedOperatingSystems.ContainsOne(os) { + file.mode = fileMode + } + a.files.Add(file) +} + +func (a *Archive) AddSrcOsDstFile(source Renders[FileData], sourceIgnoreFn SourceIgnoreFn, destinationFn DestinationFn, destinationsFn DestinationsFnMap, fileMode os.FileMode, perGame bool) { + if destinationFn == nil { + destinationFn = func(source string) Renders[FileData] { + return LiteralString[FileData](source) + } + } + if destinationsFn == nil { + destinationsFn = make(DestinationsFnMap) + } + destinationsFns := make(map[OperatingSystem][]DestinationFn) + for oses := range *a.targets { + destinationsFns[oses] = []DestinationFn{destinationFn} + if value, exists := destinationsFn[oses]; exists { + destinationsFns[oses] = append(destinationsFns[oses], value...) + } + } + for operatingSystem, osDestinationFns := range destinationsFns { + fileData := NewFileData(operatingSystem) + if perGame { + for game := range common.SupportedGames.Iter() { + fileData.Game = game + a.addFile(operatingSystem, fileMode, fileData, source, sourceIgnoreFn, osDestinationFns...) + } + } else { + a.addFile(operatingSystem, fileMode, fileData, source, sourceIgnoreFn, osDestinationFns...) + } + } +} + +func defaultDest(source string) string { + src := filepath.ToSlash(filepath.Clean(source)) + parts := strings.SplitN(src, "/", 2) + if len(parts) == 2 { + return parts[1] + } + return src +} + +func (a *Archive) AddScriptFiles(destDir string, source Renders[FileData], sourceIgnoreFn SourceIgnoreFn, destinationsFn DestinationsFnMap, perGame bool) { + finalDestinationsFn := destinationsFn + if finalDestinationsFn == nil { + finalDestinationsFn = make(DestinationsFnMap) + } + if _, exists := finalDestinationsFn[OSMacOS]; !exists { + finalDestinationsFn[OSMacOS] = []DestinationFn{} + } + finalDestinationsFn[OSMacOS] = append([]DestinationFn{extChange("command")}, finalDestinationsFn[OSMacOS]...) + a.AddSrcOsDstFile( + source, + sourceIgnoreFn, + func(source string) Renders[FileData] { + return LiteralString[FileData](filepath.Join(destDir, filepath.Base(source))) + }, + finalDestinationsFn, + 0744, + perGame, + ) +} + +func (a *Archive) AddConfigFiles(destDir string, source Renders[FileData], perGame bool) { + a.AddSrcOsDstFile( + source, + nil, + func(source string) Renders[FileData] { + return NewTemplate[FileData]( + filepath.Join( + destDir, + strings.ReplaceAll(defaultDest(source), `game`, `{{.Game}}`), + ), + ) + }, + nil, + 0, + perGame, + ) +} + +func (a *Archive) AddDocFiles(destDir string, destinationFn DestinationFn, destinationsFn DestinationsFnMap, sources ...string) { + finalDestinationsFn := destinationsFn + if finalDestinationsFn == nil { + finalDestinationsFn = make(DestinationsFnMap) + } + if _, exists := finalDestinationsFn[OSWindows]; !exists { + finalDestinationsFn[OSWindows] = []DestinationFn{} + } + if _, exists := finalDestinationsFn[OSMacOS]; !exists { + finalDestinationsFn[OSMacOS] = []DestinationFn{} + } + if _, exists := finalDestinationsFn[OSLinux]; !exists { + finalDestinationsFn[OSLinux] = []DestinationFn{} + } + if destinationFn == nil { + destinationFn = func(source string) Renders[FileData] { + return LiteralString[FileData](source) + } + } + finalDestinationsFn[OSWindows] = append([]DestinationFn{extChange("txt")}, finalDestinationsFn[OSWindows]...) + finalDestinationsFn[OSMacOS] = append([]DestinationFn{extChange("")}, finalDestinationsFn[OSMacOS]...) + finalDestinationsFn[OSLinux] = append([]DestinationFn{extChange("")}, finalDestinationsFn[OSLinux]...) + for _, source := range sources { + a.AddSrcOsDstFile( + LiteralString[FileData](source), + nil, + func(source string) Renders[FileData] { + return destinationFn(filepath.Join(destDir, defaultDest(source))) + }, + finalDestinationsFn, + 0, + false, + ) + } +} + +func (a *Archive) AddMainBinary(binary *Binary) { + a.binaries[filepath.Base(binary.main)] = binary +} + +func (a *Archive) AddAuxiliarBinary(binary *Binary) { + a.binaries[filepath.ToSlash(filepath.Join("bin", binary.main))] = binary +} + +func (a *Archive) CloneWithFilesPrefix(prefix string) *Archive { + newArchive := NewArchive(a.name, a.targets) + for file := range a.files.Iter() { + newFile := file + newFile.destination = filepath.ToSlash(filepath.Join(prefix, file.destination)) + newArchive.files.Add(newFile) + } + for path, binary := range a.binaries { + newArchive.binaries[filepath.ToSlash(filepath.Join(prefix, path))] = binary + } + return newArchive +} + +func archToValues(architecture Architecture, b config.Build) mapset.Set[string] { + res := mapset.NewSet[string]() + switch architecture { + case Arch386: + res.Append(b.Go386...) + case ArchAmd64: + res.Append(b.Goamd64...) + case ArchArm32: + res.Append(b.Goarm...) + case ArchArm64: + res.Append(b.Goarm64...) + } + return res +} + +func build(path, main string, operatingSystem OperatingSystem, architecture Architecture, instructionSet string) config.Build { + b := config.Build{ + ID: buildName(path, operatingSystem, &architecture, instructionSet), + Main: main, + Binary: path, + Goos: []string{operatingSystem.Name()}, + Goarch: []string{architecture.Name()}, + } + if instructionSet != "" { + switch architecture { + case Arch386: + b.Go386 = []string{instructionSet} + case ArchAmd64: + b.Goamd64 = []string{instructionSet} + case ArchArm32: + b.Goarm = []string{instructionSet} + case ArchArm64: + b.Goarm64 = []string{instructionSet} + } + } + return b +} + +func buildName(path string, operatingSystem OperatingSystem, architecture *Architecture, instructionSet string) string { + name := fmt.Sprintf("%s_%s", path, operatingSystem.Name()) + if architecture != nil { + name = fmt.Sprintf("%s_%s", name, (*architecture).Name()) + } + if instructionSet != "" { + name = fmt.Sprintf("%s_%s", name, instructionSet) + } + return name +} + +func mergeBuilds(path string, main string, operatingSystem OperatingSystem, builds ...config.Build) config.Build { + b := config.Build{ + ID: buildName(path, operatingSystem, nil, ""), + Main: main, + Binary: path, + Goos: []string{operatingSystem.Name()}, + } + for _, currentBuild := range builds { + b.Goarch = append(b.Goarch, currentBuild.Goarch...) + if len(currentBuild.Goarm) > 0 { + b.Goarm = append(b.Goarm, currentBuild.Goarm...) + } + if len(currentBuild.Goarm64) > 0 { + b.Goarm64 = append(b.Goarm64, currentBuild.Goarm64...) + } + if len(currentBuild.Go386) > 0 { + b.Go386 = append(b.Go386, currentBuild.Go386...) + } + if len(currentBuild.Goamd64) > 0 { + b.Goamd64 = append(b.Goamd64, currentBuild.Goamd64...) + } + } + return b +} + +func (a *Archive) Builds(mergedOSes ...OperatingSystem) []config.Build { + osPathMainBuilds := make(map[OperatingSystem]map[string]map[string][]config.Build) + for path, binary := range a.binaries { + for operatingSystem, architectures := range *binary.targets { + if _, exists := osPathMainBuilds[operatingSystem]; !exists { + osPathMainBuilds[operatingSystem] = make(map[string]map[string][]config.Build) + } + if _, exists := osPathMainBuilds[operatingSystem][path]; !exists { + osPathMainBuilds[operatingSystem][path] = make(map[string][]config.Build) + } + osPathMainBuilds[operatingSystem][path][binary.main] = []config.Build{} + for architecture, instructionSets := range architectures { + if instructionSets.Cardinality() > 0 { + for intructionSet := range instructionSets.Iter() { + osPathMainBuilds[operatingSystem][path][binary.main] = append(osPathMainBuilds[operatingSystem][path][binary.main], build(path, binary.main, operatingSystem, architecture, intructionSet)) + } + } else { + osPathMainBuilds[operatingSystem][path][binary.main] = append(osPathMainBuilds[operatingSystem][path][binary.main], build(path, binary.main, operatingSystem, architecture, "")) + } + } + } + } + var builds []config.Build + for operatingSystem, blds := range osPathMainBuilds { + if !slices.Contains(mergedOSes, operatingSystem) { + for _, mainBuilds := range blds { + for _, currentBlds := range mainBuilds { + builds = append(builds, currentBlds...) + } + } + continue + } + for path, mainBuilds := range blds { + for main, currentBlds := range mainBuilds { + builds = append(builds, mergeBuilds(path, main, operatingSystem, currentBlds...)) + } + } + } + return builds +} + +func archive(name string, operatingSystem OperatingSystem, architecture Architecture, instructionSet string, allBuilds []config.Build, files mapset.Set[File]) *config.Archive { + matchingBuildIds := mapset.NewSet[string]() + for _, b := range allBuilds { + if slices.Contains(b.Goos, operatingSystem.Name()) && slices.Contains(b.Goarch, architecture.Name()) { + if instructionSet == "" { + matchingBuildIds.Add(b.ID) + } else if archValues := archToValues(architecture, b); archValues.Contains(instructionSet) { + matchingBuildIds.Add(b.ID) + } + } + } + if matchingBuildIds.IsEmpty() { + return nil + } + id := fmt.Sprintf("%s_%s_%s", name, operatingSystem.Name(), architecture.Name()) + nameTemplate := fmt.Sprintf(`{{ .ProjectName }}_%s_{{ .RawVersion }}_%s_%s`, name, operatingSystem.FriendlyName(), architecture.FriendlyName()) + if instructionSet != "" { + id = fmt.Sprintf("%s-%s", id, instructionSet) + nameTemplate = fmt.Sprintf(`%s-%s`, nameTemplate, instructionSet) + } + formats := mapset.NewSet[string]() + if operatingSystem == OSWindows { + formats.Add("zip") + } else { + formats.Add("tar.gz") + } + a := &config.Archive{ + IDs: matchingBuildIds.ToSlice(), + ID: id, + NameTemplate: nameTemplate, + Formats: formats.ToSlice(), + } + for file := range files.Iter() { + if file.os == nil || *file.os == operatingSystem { + f := config.File{ + Source: file.source, + Destination: file.destination, + } + if file.mode != 0 { + f.Info.Mode = file.mode + } + a.Files = append(a.Files, f) + } + } + return a +} + +func archiveToUniversal(archive config.Archive) config.Archive { + splitId := strings.Split(archive.ID, "_") + a := config.Archive{ + ID: strings.Join(splitId[:2], "_"), + IDs: archive.IDs, + Files: archive.Files, + NameTemplate: fmt.Sprintf(`{{ .ProjectName }}_%s_{{ .RawVersion }}_mac`, splitId[0]), + Formats: archive.Formats, + } + return a +} + +func keyFromStrings(s []string) string { + elems := slices.Sorted[string](slices.Values(s)) + return strings.Join(elems, ",") +} + +func (a *Archive) Archives(builds []config.Build) []config.Archive { + binaryIdsArchives := make(map[string][]config.Archive) + for operatingSystem, architectures := range *a.targets { + for architecture, instructionSets := range architectures { + if instructionSets.Cardinality() == 0 { + if currentArchive := archive(a.name, operatingSystem, architecture, "", builds, a.files); currentArchive != nil { + id := keyFromStrings(currentArchive.IDs) + if _, exists := binaryIdsArchives[id]; !exists { + binaryIdsArchives[id] = []config.Archive{} + } + binaryIdsArchives[id] = append(binaryIdsArchives[id], *currentArchive) + } + } else { + for intructionSet := range instructionSets.Iter() { + if currentArchive := archive(a.name, operatingSystem, architecture, intructionSet, builds, a.files); currentArchive != nil { + id := keyFromStrings(currentArchive.IDs) + if _, exists := binaryIdsArchives[id]; !exists { + binaryIdsArchives[id] = []config.Archive{} + } else { + fmt.Println("as") + } + binaryIdsArchives[id] = append(binaryIdsArchives[id], *currentArchive) + } + } + } + } + } + var archives []config.Archive + for _, archs := range binaryIdsArchives { + if len(archs) == 1 { + archives = append(archives, archs[0]) + } else { + archives = append(archives, archiveToUniversal(archs[0])) + } + } + return archives +} + +func (a *Archive) RemoveFiles(sources ...string) { + for _, file := range a.files.ToSlice() { + if slices.Contains(sources, file.source) { + a.files.Remove(file) + } + } +} diff --git a/tools/scripts/internal/goreleaser/binary.go b/tools/scripts/internal/goreleaser/binary.go new file mode 100644 index 00000000..c85b39bb --- /dev/null +++ b/tools/scripts/internal/goreleaser/binary.go @@ -0,0 +1,78 @@ +package goreleaser + +import mapset "github.com/deckarep/golang-set/v2" + +type BinaryTargets map[OperatingSystem]map[Architecture]mapset.Set[string] + +func NewBinaryTargets() *BinaryTargets { + targets := make(BinaryTargets) + return &targets +} + +func (bt *BinaryTargets) Clone() *BinaryTargets { + clone := NewBinaryTargets() + clone.AddMultipleTargets(bt) + return clone +} + +func (bt *BinaryTargets) CloneForOperatingSystems(operatingSystems mapset.Set[OperatingSystem]) *BinaryTargets { + clone := NewBinaryTargets() + for os, currentTargets := range *bt { + if operatingSystems.Contains(os) { + for arch, instructionSets := range currentTargets { + clone.AddTarget(os, arch, instructionSets.ToSlice()...) + } + } + } + return clone +} + +func (bt *BinaryTargets) AddTarget(os OperatingSystem, arch Architecture, instructionSets ...string) { + if !os.Archs().ContainsOne(arch) { + panic("unsupported architecture for operating system") + } + instructionSetsSet := mapset.NewSet[string](instructionSets...) + if !instructionSetsSet.IsSubset(arch.InstructionSet()) { + panic("unsupported instruction set for architecture") + } + if _, ok := (*bt)[os]; !ok { + (*bt)[os] = make(map[Architecture]mapset.Set[string]) + } + if _, ok := (*bt)[os][arch]; !ok { + (*bt)[os][arch] = mapset.NewSet[string]() + } + (*bt)[os][arch] = (*bt)[os][arch].Union(instructionSetsSet) +} + +func (bt *BinaryTargets) AddMultipleTargets(multipleTargets ...*BinaryTargets) { + for _, targets := range multipleTargets { + for os, archs := range *targets { + for arch, instructionSets := range archs { + (*bt).AddTarget(os, arch, instructionSets.ToSlice()...) + } + } + } +} + +type Binary struct { + targets *BinaryTargets + main string +} + +func NewBinary(main string, targets *BinaryTargets) *Binary { + return &Binary{ + main: main, + targets: targets, + } +} + +func (b *Binary) Main() string { + return b.main +} + +func (b *Binary) CloneForOperatingSystems(operatingSystems mapset.Set[OperatingSystem]) *Binary { + return &Binary{ + main: b.main, + targets: b.targets.CloneForOperatingSystems(operatingSystems), + } +} diff --git a/tools/scripts/internal/goreleaser/config.go b/tools/scripts/internal/goreleaser/config.go new file mode 100644 index 00000000..d5b7754f --- /dev/null +++ b/tools/scripts/internal/goreleaser/config.go @@ -0,0 +1,70 @@ +package goreleaser + +import ( + "fmt" + "path/filepath" + "strings" +) + +const configSource = `%s/resources/config.game.toml` +const scriptSource = `%s/resources/{{.BaseOS}}/%s.{{.SrcScriptExt}}` +const gameScriptSource = `%s/resources/{{.BaseOS}}/start_{{.Game}}.{{.SrcScriptExt}}` + +func Generate() error { + // Server Archive + serverArchive := NewArchive("server", Targets3264) + serverArchive.AddDocFiles("docs", nil, nil, "LICENSE", "server/README.md", "server/BattleServers.md") + serverArchive.AddSrcDstFile("server/resources/responses", "resources/responses") + serverArchive.AddSrcDstFile("server/resources/config", "resources/config") + serverArchive.AddScriptFiles("", NewTemplate[FileData](fmt.Sprintf(gameScriptSource, `server`)), nil, nil, true) + serverArchive.AddScriptFiles("bin", NewTemplate[FileData](fmt.Sprintf(scriptSource, `server-genCert`, `genCert`)), SourceIgnoreFn{ + OSWindows: func(path string) bool { + return path == `server-genCert/resources/windows/genCert.bat` + }, + OSMacOS: func(path string) bool { + return path == `server-genCert/resources/unix/genCert.sh` + }, + }, nil, false) + server := NewBinary("./server", Targets3264) + serverArchive.AddMainBinary(server) + serverGenCert := NewBinary("./server-genCert", Targets3264) + serverArchive.AddAuxiliarBinary(serverGenCert) + // Battle Server Manager Archive + battleServerManagerArchive := NewArchive("battle-server-manager", Targets64ExceptMacOS) + battleServerManagerArchive.AddDocFiles("docs", nil, nil, "battle-server-manager/README.md") + battleServerManagerArchive.AddScriptFiles("", NewTemplate[FileData](fmt.Sprintf(gameScriptSource, `battle-server-manager`)), nil, nil, true) + battleServerManagerArchive.AddScriptFiles("", NewTemplate[FileData](fmt.Sprintf(scriptSource, `battle-server-manager`, `clean`)), nil, nil, false) + battleServerManagerArchive.AddScriptFiles("", NewTemplate[FileData](fmt.Sprintf(scriptSource, `battle-server-manager`, `remove-all`)), nil, nil, false) + battleServerManagerArchive.AddConfigFiles("", NewTemplate[FileData](fmt.Sprintf(configSource, `battle-server-manager`)), true) + battleServerManager := NewBinary("./battle-server-manager", Targets64ExceptMacOS) + battleServerManagerArchive.AddMainBinary(battleServerManager) + // Launcher archive + launcherArchive := NewArchive("launcher", Targets64ExceptMacOS) + launcherArchive.AddSrcDstFile("launcher/resources/config.toml", "resources/config.toml") + launcherArchive.AddScriptFiles("", NewTemplate[FileData](fmt.Sprintf(gameScriptSource, `launcher`)), nil, nil, true) + launcherArchive.AddConfigFiles("", NewTemplate[FileData](fmt.Sprintf(configSource, `launcher`)), true) + launcherArchive.AddDocFiles("docs", nil, nil, "launcher/README.md", "LICENSE") + launcherArchive.AddDocFiles("docs", func(source string) Renders[FileData] { + ext := filepath.Ext(source) + root := strings.TrimSuffix(source, ext) + return LiteralString[FileData](fmt.Sprintf("%s%s%s", root, "-config", ext)) + }, nil, "launcher-config/README.md") + launcher := NewBinary("./launcher", Targets64ExceptMacOS) + launcherArchive.AddMainBinary(launcher) + launcherAgent := NewBinary("./launcher-agent", Targets64ExceptMacOS) + launcherArchive.AddAuxiliarBinary(launcherAgent) + launcherConfig := NewBinary("./launcher-config", Targets64ExceptMacOS) + launcherArchive.AddAuxiliarBinary(launcherConfig) + launcherConfigAdmin := NewBinary("./launcher-config-admin", Targets64ExceptMacOS) + launcherArchive.AddAuxiliarBinary(launcherConfigAdmin) + launcherConfigAdminAgent := NewBinary("./launcher-config-admin-agent", Targets64ExceptMacOS) + launcherArchive.AddAuxiliarBinary(launcherConfigAdminAgent) + // Full archive + fullServerArchive := serverArchive.CloneWithFilesPrefix(`server`) + fullLauncherArchive := launcherArchive.CloneWithFilesPrefix(`launcher`) + fullBattleServerManager := battleServerManagerArchive.CloneWithFilesPrefix(`battle-server-manager`) + fullArchive := NewMergedArchive("full", fullServerArchive, fullLauncherArchive, fullBattleServerManager) + fullArchive.RemoveFiles("LICENSE") + fullArchive.AddDocFiles("docs", nil, nil, "LICENSE", "README.md") + return GenerateConfig(serverArchive, battleServerManagerArchive, launcherArchive, fullArchive) +} diff --git a/tools/scripts/internal/goreleaser/constants.go b/tools/scripts/internal/goreleaser/constants.go new file mode 100644 index 00000000..9cf4c444 --- /dev/null +++ b/tools/scripts/internal/goreleaser/constants.go @@ -0,0 +1,39 @@ +package goreleaser + +import mapset "github.com/deckarep/golang-set/v2" + +var UnixBasedOperatingSystems = mapset.NewSet(OSLinux, OSMacOS) +var operatingSystems = UnixBasedOperatingSystems.Union(mapset.NewSet(OSWindows)) +var Targets64 *BinaryTargets +var Targets64ExceptMacOS *BinaryTargets +var Targets32 *BinaryTargets +var Targets3264 *BinaryTargets +var x64Architectures = []Architecture{ArchAmd64, ArchArm64} +var x86Architectures = []Architecture{Arch386, ArchArm32} + +func init() { + Targets32 = NewBinaryTargets() + Targets64 = NewBinaryTargets() + Targets64ExceptMacOS = NewBinaryTargets() + for os := range operatingSystems.Iter() { + for _, arch := range x86Architectures { + if os.Archs().ContainsOne(arch) { + var instructionSets []string + if arch == ArchArm32 { + instructionSets = []string{"5", "6"} + } + Targets32.AddTarget(os, arch, instructionSets...) + } + } + for _, arch := range x64Architectures { + if os.Archs().ContainsOne(arch) { + Targets64.AddTarget(os, arch) + if os != OSMacOS { + Targets64ExceptMacOS.AddTarget(os, arch) + } + } + } + } + Targets3264 = Targets32.Clone() + Targets3264.AddMultipleTargets(Targets64) +} diff --git a/tools/scripts/internal/goreleaser/os.go b/tools/scripts/internal/goreleaser/os.go new file mode 100644 index 00000000..6b868658 --- /dev/null +++ b/tools/scripts/internal/goreleaser/os.go @@ -0,0 +1,142 @@ +package goreleaser + +import mapset "github.com/deckarep/golang-set/v2" + +var ( + Arch386 Architecture = X8632{} + ArchAmd64 Architecture = X8664{} + ArchArm32 Architecture = Arm32{} + ArchArm64 Architecture = Arm64{} +) + +var ( + OSWindows OperatingSystem = Windows{} + OSLinux OperatingSystem = Linux{} + OSMacOS OperatingSystem = MacOS{} +) + +type OperatingSystem interface { + Name() string + FriendlyName() string + Archs() mapset.Set[Architecture] +} + +type Architecture interface { + Name() string + InstructionSet() mapset.Set[string] + FriendlyName() string +} + +type X8632 struct{} + +func (a X8632) Name() string { + return "386" +} + +func (a X8632) FriendlyName() string { + return "x86-32" +} + +func (a X8632) InstructionSet() mapset.Set[string] { + return mapset.NewSet[string]("386", "softfloat") +} + +type X8664 struct{} + +func (a X8664) Name() string { + return "amd64" +} + +func (a X8664) FriendlyName() string { + return "x86-64" +} + +func (a X8664) InstructionSet() mapset.Set[string] { + return mapset.NewSet[string]("v1", "v2", "v3", "v4") +} + +type Arm32 struct{} + +func (a Arm32) Name() string { + return "arm" +} + +func (a Arm32) FriendlyName() string { + return a.Name() +} + +func (a Arm32) InstructionSet() mapset.Set[string] { + return mapset.NewSet[string]("5", "6", "7") +} + +type Arm64 struct{} + +func (a Arm64) Name() string { + return "arm64" +} + +func (a Arm64) FriendlyName() string { + return a.Name() +} + +func (a Arm64) InstructionSet() mapset.Set[string] { + base := []string{ + "v8.0", "v8.1", "v8.2", "v8.3", "v8.4", "v8.5", "v8.6", "v8.7", "v8.8", "v8.9", + "v9.0", "v9.1", "v9.2", "v9.3", "v9.4", "v9.5", + } + suffixes := []string{ + "", + ",lse", + ",crypto", + ",lse,crypto", + } + set := mapset.NewSet[string]() + for _, v := range base { + for _, s := range suffixes { + set.Add(v + s) + } + } + return set +} + +type Windows struct{} + +func (w Windows) Name() string { + return "windows" +} + +func (w Windows) FriendlyName() string { + return "win10" +} + +func (w Windows) Archs() mapset.Set[Architecture] { + return mapset.NewSet[Architecture](Arch386, ArchAmd64, ArchArm64) +} + +type Linux struct{} + +func (l Linux) Name() string { + return "linux" +} + +func (l Linux) FriendlyName() string { + return l.Name() +} + +func (l Linux) Archs() mapset.Set[Architecture] { + return mapset.NewSet[Architecture](Arch386, ArchAmd64, ArchArm32, ArchArm64) +} + +type MacOS struct{} + +func (m MacOS) Name() string { + return "darwin" +} + +func (m MacOS) FriendlyName() string { + return "mac" +} + +func (m MacOS) Archs() mapset.Set[Architecture] { + return mapset.NewSet[Architecture](ArchAmd64, ArchArm64) +} diff --git a/tools/scripts/internal/goreleaser/project.go b/tools/scripts/internal/goreleaser/project.go new file mode 100644 index 00000000..977a8ec8 --- /dev/null +++ b/tools/scripts/internal/goreleaser/project.go @@ -0,0 +1,58 @@ +package goreleaser + +import ( + "os" + "slices" + + "github.com/goreleaser/goreleaser/v2/pkg/config" + "gopkg.in/yaml.v3" +) + +func universalBinaries(binaries []config.Build) []config.UniversalBinary { + var result []config.UniversalBinary + for _, binary := range binaries { + if slices.Contains(binary.Goos, OSMacOS.Name()) { + result = append(result, config.UniversalBinary{ + ID: binary.ID, + NameTemplate: binary.Binary, + Replace: true, + }) + } + } + return result +} + +func GenerateConfig(archives ...*Archive) error { + project := config.Project{ + Version: 2, + } + for _, a := range archives { + archiveBuilds := a.Builds(OSMacOS) + project.Builds = append(project.Builds, archiveBuilds...) + project.Archives = append(project.Archives, a.Archives(archiveBuilds)...) + } + project.UniversalBinaries = universalBinaries(project.Builds) + + project.Checksum = config.Checksum{ + NameTemplate: `{{ .ProjectName }}_{{ .RawVersion }}_checksums.txt`, + } + project.Signs = []config.Sign{ + { + Artifacts: "checksum", + Cmd: "gpg2", + Args: []string{"--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"}, + }, + } + project.Release = config.Release{ + Draft: true, + ReplaceExistingDraft: false, + UseExistingDraft: true, + ReplaceExistingArtifacts: true, + Prerelease: `auto`, + } + marshal, err := yaml.Marshal(&project) + if err != nil { + return err + } + return os.WriteFile(".goreleaser.yaml", marshal, 0o644) +} diff --git a/tools/scripts/internal/goreleaser/template.go b/tools/scripts/internal/goreleaser/template.go new file mode 100644 index 00000000..5701de8b --- /dev/null +++ b/tools/scripts/internal/goreleaser/template.go @@ -0,0 +1,39 @@ +package goreleaser + +import ( + "bytes" + "text/template" + + "github.com/google/uuid" +) + +type Renders[D any] interface { + Render(data D) string +} +type LiteralString[D any] string + +func (t LiteralString[D]) Render(_ D) string { + return string(t) +} + +type Template[D any] struct { + tmpl *template.Template + text string +} + +func NewTemplate[D any](text string) *Template[D] { + tmpl := template.New(uuid.NewString()) + return &Template[D]{tmpl: tmpl, text: text} +} + +func (t *Template[D]) Render(data D) string { + if tmpl, err := t.tmpl.Parse(t.text); err != nil { + return "" + } else { + var buf bytes.Buffer + if err = tmpl.Execute(&buf, data); err != nil { + buf.WriteString("") + } + return buf.String() + } +} diff --git a/server-docker/README.md b/tools/server-docker/README.md similarity index 100% rename from server-docker/README.md rename to tools/server-docker/README.md diff --git a/server-docker/compose.yml b/tools/server-docker/compose.yml similarity index 100% rename from server-docker/compose.yml rename to tools/server-docker/compose.yml diff --git a/server-docker/genCert/Dockerfile b/tools/server-docker/genCert/Dockerfile similarity index 89% rename from server-docker/genCert/Dockerfile rename to tools/server-docker/genCert/Dockerfile index 0e0551c1..346e1490 100644 --- a/server-docker/genCert/Dockerfile +++ b/tools/server-docker/genCert/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # Compile -FROM golang:1.24-alpine3.22 AS compiler +FROM golang:1.25-alpine3.23 AS compiler WORKDIR /app COPY server-docker/genCert/go.work.template go.work COPY common common @@ -10,7 +10,7 @@ RUN mkdir -p build/resources/certificates RUN mkdir build/bin RUN go build -ldflags="-s -w" -o build/bin/genCert ./server-genCert # Compress -FROM alpine:3.22 AS compressor +FROM alpine:3.23 AS compressor RUN apk add --no-cache upx WORKDIR /app COPY --from=compiler /app/build . diff --git a/server-docker/genCert/go.work.template b/tools/server-docker/genCert/go.work.template similarity index 76% rename from server-docker/genCert/go.work.template rename to tools/server-docker/genCert/go.work.template index abaa002b..e2fc4490 100644 --- a/server-docker/genCert/go.work.template +++ b/tools/server-docker/genCert/go.work.template @@ -1,4 +1,4 @@ -go 1.24.0 +go 1.25.5 use ( common diff --git a/server-docker/server/Dockerfile b/tools/server-docker/server/Dockerfile similarity index 90% rename from server-docker/server/Dockerfile rename to tools/server-docker/server/Dockerfile index b43c4900..80908938 100644 --- a/server-docker/server/Dockerfile +++ b/tools/server-docker/server/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # Compile -FROM golang:1.24-alpine3.22 AS compiler +FROM golang:1.25-alpine3.23 AS compiler WORKDIR /app COPY server-docker/server/go.work.template go.work COPY common common @@ -11,7 +11,7 @@ RUN cp -r server/resources build/resources && rm -rf build/resources/windows && RUN mkdir -p build/resources/certificates RUN go build -ldflags="-s -w" -o build/server ./server # Compress -FROM alpine:3.22 AS compressor +FROM alpine:3.23 AS compressor RUN apk add --no-cache upx WORKDIR /app COPY --from=compiler /app/build/server . diff --git a/server-docker/server/go.work.template b/tools/server-docker/server/go.work.template similarity index 70% rename from server-docker/server/go.work.template rename to tools/server-docker/server/go.work.template index 6ebade33..66f93294 100644 --- a/server-docker/server/go.work.template +++ b/tools/server-docker/server/go.work.template @@ -1,4 +1,4 @@ -go 1.24.0 +go 1.25.5 use ( common diff --git a/server-replay/README.MD b/tools/server-replay/README.MD similarity index 100% rename from server-replay/README.MD rename to tools/server-replay/README.MD diff --git a/server-replay/go.mod b/tools/server-replay/go.mod similarity index 72% rename from server-replay/go.mod rename to tools/server-replay/go.mod index 9fab709a..587c1d63 100644 --- a/server-replay/go.mod +++ b/tools/server-replay/go.mod @@ -1,6 +1,6 @@ module github.com/luskaner/ageLANServer/server-replay -go 1.24.0 +go 1.25.5 require ( github.com/gorilla/websocket v1.5.3 @@ -9,7 +9,9 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect diff --git a/tools/server-replay/go.sum b/tools/server-replay/go.sum new file mode 100644 index 00000000..527e6335 --- /dev/null +++ b/tools/server-replay/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/r3labs/diff/v3 v3.0.2 h1:yVuxAY1V6MeM4+HNur92xkS39kB/N+cFi2hMkY06BbA= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/server-replay/internal/client/client.go b/tools/server-replay/internal/client/client.go similarity index 100% rename from server-replay/internal/client/client.go rename to tools/server-replay/internal/client/client.go diff --git a/server-replay/internal/client/http/http.go b/tools/server-replay/internal/client/http/http.go similarity index 100% rename from server-replay/internal/client/http/http.go rename to tools/server-replay/internal/client/http/http.go diff --git a/server-replay/internal/client/wss/wss.go b/tools/server-replay/internal/client/wss/wss.go similarity index 100% rename from server-replay/internal/client/wss/wss.go rename to tools/server-replay/internal/client/wss/wss.go diff --git a/server-replay/internal/cmd/root.go b/tools/server-replay/internal/cmd/root.go similarity index 100% rename from server-replay/internal/cmd/root.go rename to tools/server-replay/internal/cmd/root.go diff --git a/server-replay/internal/decoder/decoder.go b/tools/server-replay/internal/decoder/decoder.go similarity index 100% rename from server-replay/internal/decoder/decoder.go rename to tools/server-replay/internal/decoder/decoder.go diff --git a/server-replay/internal/logEntry/http/http.go b/tools/server-replay/internal/logEntry/http/http.go similarity index 100% rename from server-replay/internal/logEntry/http/http.go rename to tools/server-replay/internal/logEntry/http/http.go diff --git a/server-replay/internal/logEntry/logEntry.go b/tools/server-replay/internal/logEntry/logEntry.go similarity index 100% rename from server-replay/internal/logEntry/logEntry.go rename to tools/server-replay/internal/logEntry/logEntry.go diff --git a/server-replay/internal/logEntry/wss/connection.go b/tools/server-replay/internal/logEntry/wss/connection.go similarity index 100% rename from server-replay/internal/logEntry/wss/connection.go rename to tools/server-replay/internal/logEntry/wss/connection.go diff --git a/server-replay/internal/logEntry/wss/data.go b/tools/server-replay/internal/logEntry/wss/data.go similarity index 100% rename from server-replay/internal/logEntry/wss/data.go rename to tools/server-replay/internal/logEntry/wss/data.go diff --git a/server-replay/internal/logEntry/wss/disconnection.go b/tools/server-replay/internal/logEntry/wss/disconnection.go similarity index 100% rename from server-replay/internal/logEntry/wss/disconnection.go rename to tools/server-replay/internal/logEntry/wss/disconnection.go diff --git a/server-replay/internal/logEntry/wss/wss.go b/tools/server-replay/internal/logEntry/wss/wss.go similarity index 100% rename from server-replay/internal/logEntry/wss/wss.go rename to tools/server-replay/internal/logEntry/wss/wss.go diff --git a/server-replay/internal/response.go b/tools/server-replay/internal/response.go similarity index 100% rename from server-replay/internal/response.go rename to tools/server-replay/internal/response.go diff --git a/server-replay/internal/server/configWriter.go b/tools/server-replay/internal/server/configWriter.go similarity index 100% rename from server-replay/internal/server/configWriter.go rename to tools/server-replay/internal/server/configWriter.go diff --git a/server-replay/internal/server/executor.go b/tools/server-replay/internal/server/executor.go similarity index 100% rename from server-replay/internal/server/executor.go rename to tools/server-replay/internal/server/executor.go diff --git a/server-replay/main.go b/tools/server-replay/main.go similarity index 100% rename from server-replay/main.go rename to tools/server-replay/main.go