Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e94093ac17 | |||
| c4e7302567 | |||
| f51eea6c0f | |||
| befb377774 | |||
| fad862d618 | |||
| fb8a3f5658 | |||
| f2111a376a | |||
| da03ba11c0 | |||
| c2313e9163 | |||
| 2f0a3ee8d4 | |||
| 15ee7833ed | |||
| e79f3fa362 | |||
| e87d4083e9 | |||
| 5a18b41528 | |||
| d6fd40969f | |||
| a3d24114a1 | |||
| 8867da569a | |||
| 1796829338 | |||
| f246efd6ad | |||
| f25b77b605 | |||
| a09e3df220 | |||
| 26347fca30 | |||
| 5ea65a7e9b | |||
| 5f3875bc14 | |||
| cc2700d507 | |||
| 5f5cc6f894 | |||
| 7de65f6145 | |||
| 85a13eb241 | |||
| dea744bc30 | |||
| c22b21a537 | |||
| b44f92aacf | |||
| 0162084e53 | |||
| 2f4b6a98d8 | |||
| d90f8693c9 | |||
| e870c96879 | |||
| e531e7d9b6 | |||
| 57cc023558 | |||
| 23929e3651 | |||
| 829aadca2d | |||
| 30486616f8 | |||
| d9a2679af4 | |||
| 4f57c37572 | |||
| e4a0dd2d13 | |||
| 0a10cff0fa | |||
| e4cf5bdad0 | |||
| 645751cd23 | |||
| b92a49821d | |||
| 3a91528e95 | |||
| 3c37be2a8e | |||
| 544b5f4f56 | |||
| cc9de54d42 | |||
| e76414ce17 | |||
| 68c2b4f25d | |||
| 78946f1bb4 | |||
| 0d1671fe8f | |||
| ea35017aa0 | |||
| 0db24fec60 | |||
| b2a18c9c41 | |||
| 16ec80929c | |||
| 3536e25d52 | |||
| 935d249183 | |||
| 8cb01d77de | |||
| a446b7cb41 |
@@ -1,13 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: Godot 4.4 Core Rules
|
name: Godot 4.7 Core Rules
|
||||||
priority: high
|
priority: high
|
||||||
---
|
---
|
||||||
|
|
||||||
Ты — senior Godot 4.4 GDScript разработчик.
|
Ты — senior Godot 4.7 GDScript разработчик.
|
||||||
|
|
||||||
|
- Use Godot 4.7+ API.
|
||||||
|
- Do not use Godot 3.x syntax or APIs.
|
||||||
|
- Follow the existing project architecture and coding style.
|
||||||
|
- Do not introduce unnecessary abstractions.
|
||||||
|
- Do not modify unrelated files.
|
||||||
|
|
||||||
- Всегда используй Godot 4.4+ API.
|
|
||||||
- Пиши строго типизированный код (`var health: int = 100`, `: Array[Node]` и т.д.).
|
- Пиши строго типизированный код (`var health: int = 100`, `: Array[Node]` и т.д.).
|
||||||
- Предпочитай Signals вместо проверки в _process.
|
- Предпочитай Signals вместо проверки в _process.
|
||||||
- Используй @export, @export_range, @export_enum, @onready.
|
- Используй @export, @export_range, @export_enum, @onready.
|
||||||
- Composition over deep inheritance.
|
- Composition over deep inheritance.
|
||||||
- Менеджеры и автолоады выноси в отдельные singleton'ы.
|
- Менеджеры и автолоады выноси в отдельные singleton'ы.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
- use spaces not tabs for indentation
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: Android production release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build-publish:
|
||||||
|
runs-on: godot
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Verify tag belongs to main
|
||||||
|
run: |
|
||||||
|
git fetch origin main
|
||||||
|
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
|
||||||
|
|
||||||
|
- name: Show Godot version
|
||||||
|
run: godot --version
|
||||||
|
|
||||||
|
- name: Import project
|
||||||
|
run: |
|
||||||
|
rm -rf .godot
|
||||||
|
godot --headless --path . --editor --import --quit --verbose
|
||||||
|
|
||||||
|
- name: Run all tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mapfile -t tests < <(find tests -maxdepth 1 -type f -name 'test_*.gd' | sort)
|
||||||
|
test "${#tests[@]}" -gt 0
|
||||||
|
|
||||||
|
for test_file in "${tests[@]}"; do
|
||||||
|
echo "::group::${test_file}"
|
||||||
|
godot --headless --path . --script "res://${test_file}"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Prepare Android signing
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
test -n "${ANDROID_KEYSTORE_BASE64}"
|
||||||
|
printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${RUNNER_TEMP}/release.keystore"
|
||||||
|
test -s "${RUNNER_TEMP}/release.keystore"
|
||||||
|
|
||||||
|
- name: Configure release AAB
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ github.ref_name }}
|
||||||
|
RELEASE_VERSION_CODE: ${{ github.run_number }}
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path("export_presets.cfg")
|
||||||
|
text = path.read_text()
|
||||||
|
start = text.index('[preset.1.options]')
|
||||||
|
head, android = text[:start], text[start:]
|
||||||
|
replacements = (
|
||||||
|
(r'gradle_build/use_gradle_build=false', 'gradle_build/use_gradle_build=true'),
|
||||||
|
(r'gradle_build/export_format=0', 'gradle_build/export_format=1'),
|
||||||
|
(r'package/signed=false', 'package/signed=true'),
|
||||||
|
(r'version/code=\d+', f'version/code={100000 + int(os.environ["RELEASE_VERSION_CODE"])}'),
|
||||||
|
(r'version/name="[^"]*"', f'version/name="{os.environ["RELEASE_VERSION"].removeprefix("v")}"'),
|
||||||
|
)
|
||||||
|
for pattern, replacement in replacements:
|
||||||
|
android, count = re.subn(pattern, replacement, android, count=1)
|
||||||
|
if count != 1:
|
||||||
|
raise RuntimeError(f'Expected one Android preset match for {pattern!r}, got {count}')
|
||||||
|
path.write_text(head + android)
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Build signed Android App Bundle
|
||||||
|
env:
|
||||||
|
GODOT_ANDROID_KEYSTORE_RELEASE_USER: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}
|
||||||
|
GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
export GODOT_ANDROID_KEYSTORE_RELEASE_PATH="${RUNNER_TEMP}/release.keystore"
|
||||||
|
mkdir -p build
|
||||||
|
godot --headless --path . --export-release "Android" build/owniverse-${GITHUB_REF_NAME}.aab
|
||||||
|
test -s "build/owniverse-${GITHUB_REF_NAME}.aab"
|
||||||
|
ls -lh "build/owniverse-${GITHUB_REF_NAME}.aab"
|
||||||
|
|
||||||
|
- name: Publish to Google Play production
|
||||||
|
uses: r0adkll/upload-google-play@v1
|
||||||
|
with:
|
||||||
|
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
packageName: com.KIDGameProduction.Owniverse
|
||||||
|
releaseFiles: build/owniverse-${{ github.ref_name }}.aab
|
||||||
|
tracks: production
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
- name: Upload release bundle
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: owniverse-${{ github.ref_name }}-aab
|
||||||
|
path: build/owniverse-${{ github.ref_name }}.aab
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 90
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Android
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
android:
|
|
||||||
runs-on: godot
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Godot version
|
|
||||||
run: godot --version
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: |
|
|
||||||
mkdir -p build
|
|
||||||
|
|
||||||
godot \
|
|
||||||
--headless \
|
|
||||||
--path . \
|
|
||||||
--export-release "Android" \
|
|
||||||
build/app.apk
|
|
||||||
|
|
||||||
- name: Verify APK
|
|
||||||
run: |
|
|
||||||
test -f build/app.apk
|
|
||||||
ls -lh build/app.apk
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
run: |
|
|
||||||
curl -f \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases" \
|
|
||||||
-d "{
|
|
||||||
\"tag_name\": \"${GITHUB_REF_NAME}\",
|
|
||||||
\"name\": \"${GITHUB_REF_NAME}\",
|
|
||||||
\"body\": \"Android build ${GITHUB_REF_NAME}\"
|
|
||||||
}"
|
|
||||||
|
|
||||||
- name: Upload APK to Release
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
run: |
|
|
||||||
RELEASE_ID=$(curl -fsS \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases/tags/${GITHUB_REF_NAME}" \
|
|
||||||
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
|
||||||
|
|
||||||
curl -f \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-F "attachment=@build/app.apk" \
|
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases/${RELEASE_ID}/assets?name=owniverse-${GITHUB_REF_NAME}.apk"
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: Testing builds
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-and-build:
|
||||||
|
runs-on: godot
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Show Godot version
|
||||||
|
run: godot --version
|
||||||
|
|
||||||
|
- name: Import project
|
||||||
|
run: |
|
||||||
|
rm -rf .godot
|
||||||
|
godot --headless --path . --editor --import --quit --verbose
|
||||||
|
|
||||||
|
- name: Run all tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mapfile -t tests < <(find tests -maxdepth 1 -type f -name 'test_*.gd' | sort)
|
||||||
|
test "${#tests[@]}" -gt 0
|
||||||
|
|
||||||
|
for test_file in "${tests[@]}"; do
|
||||||
|
echo "::group::${test_file}"
|
||||||
|
godot --headless --path . --script "res://${test_file}"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build Android APK
|
||||||
|
run: |
|
||||||
|
mkdir -p build
|
||||||
|
godot --headless --path . --export-release "Android" build/owniverse-testing.apk
|
||||||
|
|
||||||
|
- name: Build Windows EXE
|
||||||
|
run: |
|
||||||
|
godot --headless --path . --export-release "Windows Desktop" build/owniverse-testing.exe
|
||||||
|
|
||||||
|
- name: Verify builds
|
||||||
|
run: |
|
||||||
|
test -s build/owniverse-testing.apk
|
||||||
|
test -s build/owniverse-testing.exe
|
||||||
|
ls -lh build/owniverse-testing.apk build/owniverse-testing.exe
|
||||||
|
|
||||||
|
- name: Upload testing builds
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: owniverse-testing-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
build/owniverse-testing.apk
|
||||||
|
build/owniverse-testing.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
/android/
|
/android/
|
||||||
*.import
|
*.import
|
||||||
|
!data/*.txt.import
|
||||||
|
|
||||||
# MacOS specific ignores
|
# MacOS specific ignores
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+49
-19
@@ -1,6 +1,10 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
const SPEED_LIMIT: int = 4
|
const SPEED_LIMIT: int = 6
|
||||||
|
|
||||||
|
# Write a readable diagnostic copy next to the encrypted save. Never used for loading.
|
||||||
|
var save_plaintext_copy: bool = true
|
||||||
|
|
||||||
var Settings: Dictionary = {
|
var Settings: Dictionary = {
|
||||||
"filename": "save-0001",
|
"filename": "save-0001",
|
||||||
@@ -16,6 +20,8 @@ var Locale = "en"
|
|||||||
var OwniverseEntities: Dictionary = {}
|
var OwniverseEntities: Dictionary = {}
|
||||||
var OwniverseItems: Dictionary = {}
|
var OwniverseItems: Dictionary = {}
|
||||||
var Templates: Dictionary = {}
|
var Templates: Dictionary = {}
|
||||||
|
var LifeRules: Dictionary = {}
|
||||||
|
var CivilizationRules: Dictionary = {}
|
||||||
|
|
||||||
var batch_count: float = 40.0 # must be even not odd
|
var batch_count: float = 40.0 # must be even not odd
|
||||||
|
|
||||||
@@ -26,35 +32,59 @@ func get_localized_item_description(item_id: String, item: String) -> String:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# Russian templates include the class; legacy English keys omit it.
|
||||||
|
func get_template(id: String, category: String, field: String) -> Dictionary:
|
||||||
|
var rows: Dictionary = Templates.get(Locale, {})
|
||||||
|
for key in [id + "." + category + "." + field, id + "." + field]:
|
||||||
|
if rows.has(key):
|
||||||
|
return rows[key]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func render_template(row: Dictionary, variables: Dictionary) -> String:
|
||||||
|
var result: String = str(row.get("text", ""))
|
||||||
|
for i in range(5):
|
||||||
|
var variable: String = str(row.get("value" + str(i), "")).strip_edges()
|
||||||
|
var value: String = str(variables.get(variable, "—")) if variable != "" else ""
|
||||||
|
result = result.replace("%value" + str(i) + "%", value)
|
||||||
|
return result.replace("<br>", "\n").replace("[p]", "\n")
|
||||||
|
|
||||||
|
|
||||||
func get_description(item_id: String, items: Array = []) -> String:
|
func get_description(item_id: String, items: Array = []) -> String:
|
||||||
if !Templates[Locale].has(item_id):
|
var row: Dictionary = Templates.get(Locale, {}).get(item_id, {})
|
||||||
return ""
|
var variables: Dictionary = {}
|
||||||
var description: String = Templates[Locale][item_id]["text"]
|
for i in mini(items.size(), 5):
|
||||||
var value = 0
|
variables[str(row.get("value" + str(i), ""))] = items[i]
|
||||||
for item in items:
|
return render_template(row, variables)
|
||||||
description.replace("value" + str(value), str(item))
|
|
||||||
return description
|
|
||||||
|
|
||||||
|
|
||||||
func format_number(value: float, digits: int = 0) -> String:
|
func format_number(value, digits: int = 0) -> String:
|
||||||
if value == 0: return ""
|
return E.grouped(value, digits)
|
||||||
if value < 10_000:
|
|
||||||
return "%.*f" % [digits, value]
|
|
||||||
|
|
||||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
|
||||||
var mantissa = value / pow(10.0, exp_value)
|
|
||||||
|
|
||||||
return "%.*f" % [digits, mantissa] + "E%+d" % exp_value
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
OwniverseEntities = _read_from_csv("res://data/owniverse_entitites.txt")
|
CivilizationRules.clear()
|
||||||
|
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
|
||||||
|
if not CivilizationRules.has(row.stage):
|
||||||
|
CivilizationRules[row.stage] = {"home": row.home, "span": E.of(row.span), "stars": E.of(row.stars), "ethics": {},
|
||||||
|
"colonizes": row.colonizes == "1", "fights": row.fights == "1", "diplomacy": row.diplomacy == "1", "worships": row.worships == "1"}
|
||||||
|
CivilizationRules[row.stage].ethics[row.ethic] = row
|
||||||
|
var stages: Array = CivilizationRules.keys()
|
||||||
|
stages.sort()
|
||||||
|
for i in stages.size():
|
||||||
|
CivilizationRules[stages[i]].next = stages[i + 1] if i + 1 < stages.size() else "Ct"
|
||||||
|
CivilizationRules[stages[i]].previous = stages[i - 1] if i > 0 else "C0.1"
|
||||||
|
CivilizationRules[stages[i]].colony = stages[maxi(0, i - 2)]
|
||||||
|
CivilizationRules.Ct = {"home": "", "span": 0.0, "stars": 0.0, "ethics": {}, "next": "Ct", "previous": "C4.4", "colonizes": false, "fights": false, "diplomacy": false, "worships": false}
|
||||||
|
LifeRules = _read_from_csv("res://data/life_rules.txt")
|
||||||
|
OwniverseEntities = _read_from_csv("res://data/owniverse_entities.txt")
|
||||||
OwniverseItems = _read_from_csv("res://data/owniverse_items.txt")
|
OwniverseItems = _read_from_csv("res://data/owniverse_items.txt")
|
||||||
|
|
||||||
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
||||||
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
||||||
|
|
||||||
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
|
|
||||||
|
Templates = OwniverseTemplate.get_data()
|
||||||
|
|
||||||
|
|
||||||
func _read_from_csv(file_path: String) -> Dictionary:
|
func _read_from_csv(file_path: String) -> Dictionary:
|
||||||
|
|||||||
+319
-18
@@ -1,38 +1,339 @@
|
|||||||
# SaveManager.gd (добавить в AutoLoad)
|
# SaveManager.gd (добавить в AutoLoad)
|
||||||
extends Node
|
extends Node
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
||||||
const SETTING_SECTION := "Settings"
|
const SETTING_SECTION = "Settings"
|
||||||
const SAVE_PATH = "user://settings.cfg"
|
const SAVE_PATH = "user://settings.cfg"
|
||||||
|
|
||||||
|
const FORMAT_VERSION : int = 5
|
||||||
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const STATE_VERSION : String = SimulationTime.STATE_VERSION
|
||||||
|
var last_error: String = ""
|
||||||
|
var blocked_saves: Dictionary = {}
|
||||||
|
var save_directory: String = "user://"
|
||||||
|
signal save_completed ( filename : String , success : bool , error : String )
|
||||||
|
signal saves_drained
|
||||||
|
|
||||||
|
# Queue ownership stays on the main thread. The worker only sees its snapshot.
|
||||||
|
var _save_thread: Thread
|
||||||
|
var _active_filename: String = ""
|
||||||
|
var _pending_saves: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: Variant) -> void:
|
||||||
|
if (E.ne(_save_thread, null) and (not _save_thread.is_alive())):
|
||||||
|
_finish_save()
|
||||||
|
|
||||||
|
|
||||||
|
func is_saving() -> bool:
|
||||||
|
return (E.ne(_save_thread, null) or (not _pending_saves.is_empty()))
|
||||||
|
|
||||||
|
|
||||||
|
func _snapshot(filename: String, password: String, data: Dictionary) -> Dictionary:
|
||||||
|
var payload: Dictionary = E.encode(data)
|
||||||
|
payload.format_version = FORMAT_VERSION
|
||||||
|
payload.app_version = str(ProjectSettings.get_setting("application/config/version", ""))
|
||||||
|
return {"filename": filename, "path": save_directory.path_join(E.add(filename, ".adventure")), "password": password, "payload": payload, "plaintext": Global.save_plaintext_copy, "speed_limit": Global.SPEED_LIMIT, "stages": Global.CivilizationRules.keys(), "entities": Global.OwniverseEntities.keys()}
|
||||||
|
|
||||||
|
|
||||||
|
## True means queued, not written. Completion is reported by save_completed.
|
||||||
|
func request_save(filename: String, password: String, data: Dictionary) -> bool:
|
||||||
|
if blocked_saves.has(filename):
|
||||||
|
return _fail(E.add("Save blocked to preserve an unreadable save: ", filename))
|
||||||
|
_pending_saves[filename] = _snapshot(filename, password, data)
|
||||||
|
if E.eq(_save_thread, null):
|
||||||
|
_start_save()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _start_save() -> void:
|
||||||
|
if _pending_saves.is_empty():
|
||||||
|
return
|
||||||
|
_active_filename = _pending_saves.keys()[0]
|
||||||
|
var job: Dictionary = _pending_saves[_active_filename]
|
||||||
|
_pending_saves.erase(_active_filename)
|
||||||
|
_save_thread = Thread.new()
|
||||||
|
var error: Error = _save_thread.start(_write_snapshot.bind(job))
|
||||||
|
if E.ne(error, OK):
|
||||||
|
_save_thread = null
|
||||||
|
_fail(E.add("Cannot start save worker: ", error_string(error)))
|
||||||
|
save_completed.emit(_active_filename, false, last_error)
|
||||||
|
_start_save()
|
||||||
|
if (not is_saving()): saves_drained.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _finish_save() -> void:
|
||||||
|
var error: String = _save_thread.wait_to_finish()
|
||||||
|
var filename: String = _active_filename
|
||||||
|
_save_thread = null
|
||||||
|
last_error = error
|
||||||
|
if E.ne(error, ""): push_error(error)
|
||||||
|
save_completed.emit(filename, E.eq(error, ""), error)
|
||||||
|
if E.eq(_save_thread, null): _start_save()
|
||||||
|
if (not is_saving()): saves_drained.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Used for application suspension and teardown, when frames may stop arriving.
|
||||||
|
func flush_saves() -> void:
|
||||||
|
while is_saving():
|
||||||
|
if E.eq(_save_thread, null): _start_save()
|
||||||
|
if E.ne(_save_thread, null): _finish_save()
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
flush_saves()
|
||||||
|
|
||||||
#region Save Load routines
|
#region Save Load routines
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
last_error = message
|
||||||
|
push_error(message)
|
||||||
|
return false
|
||||||
|
|
||||||
func save_data(filename: String, password: String, data: Dictionary) -> void:
|
|
||||||
filename = "user://" + filename + ".adventure"
|
|
||||||
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.WRITE, password)
|
|
||||||
if file == null:
|
|
||||||
push_error("Can't write to the file: ", filename)
|
|
||||||
return
|
|
||||||
|
|
||||||
file.store_var(data) # бинарно + быстрее + сериализация
|
func save_data(filename: String, password: String, data: Dictionary) -> bool:
|
||||||
|
# Synchronous compatibility API must never race an asynchronous writer.
|
||||||
|
flush_saves()
|
||||||
|
last_error = ""
|
||||||
|
if blocked_saves.has(filename):
|
||||||
|
return _fail(E.add("Save blocked to preserve an unreadable save: ", filename))
|
||||||
|
var error: String = _write_snapshot(_snapshot(filename, password, data))
|
||||||
|
return (_fail(error) if E.ne(error, "") else true)
|
||||||
|
|
||||||
|
|
||||||
|
static func _write_snapshot(job: Dictionary) -> String:
|
||||||
|
var payload: Dictionary = job.payload
|
||||||
|
var problem: String = _validate_payload(payload, job.speed_limit, job.stages, job.entities)
|
||||||
|
if E.ne(problem, ""):
|
||||||
|
return E.add("Invalid save: ", problem)
|
||||||
|
var path: String = job.path
|
||||||
|
var temporary: String = E.add(path, ".tmp")
|
||||||
|
var file = FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||||
|
if E.eq(file, null):
|
||||||
|
return E.add("Cannot write encrypted save: ", temporary)
|
||||||
|
file.store_var(payload, false)
|
||||||
file.close()
|
file.close()
|
||||||
|
# Check the actual encrypted file, including finalization, before replacing the old save.
|
||||||
|
var verification = FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
||||||
|
if E.eq(verification, null):
|
||||||
|
return E.add("Cannot verify encrypted save: ", temporary)
|
||||||
|
var restored: Variant = verification.get_var(false)
|
||||||
|
verification.close()
|
||||||
|
if ((not (restored is Dictionary)) or E.ne(restored, payload)):
|
||||||
|
return E.add("Encrypted save verification failed: ", temporary)
|
||||||
|
if E.ne(DirAccess.rename_absolute(temporary, path), OK):
|
||||||
|
return E.add("Cannot replace encrypted save: ", path)
|
||||||
|
if job.plaintext:
|
||||||
|
var plain_path: String = E.add(path, ".json")
|
||||||
|
file = FileAccess.open(E.add(plain_path, ".tmp"), FileAccess.WRITE)
|
||||||
|
if E.eq(file, null):
|
||||||
|
return E.add("Encrypted save written, but cannot write diagnostic copy: ", plain_path)
|
||||||
|
file.store_string(E.add(JSON.stringify(payload, " ", true, true), "\n"))
|
||||||
|
file.flush()
|
||||||
|
var write_error: Error = file.get_error()
|
||||||
|
file.close()
|
||||||
|
if (E.ne(write_error, OK) or E.ne(DirAccess.rename_absolute(E.add(plain_path, ".tmp"), plain_path), OK)):
|
||||||
|
return E.add("Encrypted save written, but diagnostic copy failed: ", plain_path)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
func load_data(filename: String, password: String) -> Dictionary:
|
func load_data(filename: String, password: String) -> Dictionary:
|
||||||
filename = "user://" + filename + ".adventure"
|
flush_saves()
|
||||||
if not FileAccess.file_exists(filename):
|
last_error = ""
|
||||||
|
# Plaintext is diagnostic only: never use it as a fallback.
|
||||||
|
var path: String = save_directory.path_join(E.add(filename, ".adventure"))
|
||||||
|
if (not FileAccess.file_exists(path)):
|
||||||
return {}
|
return {}
|
||||||
|
var file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||||
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.READ, password)
|
if E.eq(file, null):
|
||||||
if file == null:
|
blocked_saves[filename] = true
|
||||||
push_error(filename, " couldn't be decrypted.")
|
_fail(E.add("Cannot decrypt save: ", path))
|
||||||
return {}
|
return {}
|
||||||
|
var data: Variant = file.get_var(false)
|
||||||
var data: Variant = file.get_var()
|
|
||||||
file.close()
|
file.close()
|
||||||
|
# Unsupported formats are archived, never converted or overwritten.
|
||||||
|
# Returning no data lets the stage start a new, saveable session.
|
||||||
|
if ((data is Dictionary) and E.ne(data.get("format_version"), FORMAT_VERSION)):
|
||||||
|
var archive: String = E.add(path, ".incompatible.bak")
|
||||||
|
var index: int = 0
|
||||||
|
while FileAccess.file_exists(archive):
|
||||||
|
index = E.add(index, 1)
|
||||||
|
archive = E.add(E.add(path, ".incompatible.bak."), str(index))
|
||||||
|
var error: Error = DirAccess.rename_absolute(path, archive)
|
||||||
|
if E.ne(error, OK):
|
||||||
|
blocked_saves[filename] = true
|
||||||
|
_fail(E.add("Cannot archive incompatible save: ", error_string(error)))
|
||||||
|
return {}
|
||||||
|
blocked_saves.erase(filename)
|
||||||
|
push_warning(E.add(E.add("Incompatible save preserved at ", archive), "; starting a new game."))
|
||||||
|
return {}
|
||||||
|
var problem: String = validate_data(data)
|
||||||
|
if E.ne(problem, ""):
|
||||||
|
blocked_saves[filename] = true
|
||||||
|
_fail(E.add(E.add(E.add("Invalid save ", path), ": "), problem))
|
||||||
|
return {}
|
||||||
|
blocked_saves.erase(filename)
|
||||||
|
return E.decode(data)
|
||||||
|
|
||||||
|
|
||||||
|
static func _number(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||||
|
return E.numeric(value) and E.finite(value) and (not value is float or absf(value) < 1e28) and (not whole or E.eq(value, E.floor_value(value))) and (not nonnegative or E.ge(value, 0))
|
||||||
|
|
||||||
|
|
||||||
|
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||||
|
if (not (value is Dictionary)):
|
||||||
|
return false
|
||||||
|
for key in value:
|
||||||
|
if ((not (((key is String) or (key is StringName)))) or (not _number(value[key], whole, nonnegative))):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func _plain_tree(value: Variant) -> bool:
|
||||||
|
if (value is Dictionary):
|
||||||
|
for key in value:
|
||||||
|
if ((not (((key is String) or (key is StringName)))) or (not _plain_tree(value[key]))):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
if (value is Array):
|
||||||
|
for item in value:
|
||||||
|
if (not _plain_tree(item)):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
return ((((E.eq(value, null) or (value is String)) or (value is StringName)) or (value is bool)) or _number(value, false, false))
|
||||||
|
|
||||||
|
|
||||||
|
## Return a diagnostic path on failure; an empty string means the structure is valid.
|
||||||
|
func validate_data(data: Variant) -> String:
|
||||||
|
return _validate_payload(data, Global.SPEED_LIMIT, Global.CivilizationRules.keys(), Global.OwniverseEntities.keys())
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_payload(data: Variant, speed_limit: int, stages: Array, entities: Array) -> String:
|
||||||
|
if not E.valid_encoded(data): return "invalid exact number encoding"
|
||||||
|
data = E.decode(data)
|
||||||
|
if ((not (data is Dictionary)) or (not _plain_tree(data))):
|
||||||
|
return "save must contain plain finite values"
|
||||||
|
if (E.ne(data.get("format_version"), FORMAT_VERSION) or (not (data.get("app_version") is String))):
|
||||||
|
return "unsupported save format"
|
||||||
|
if (((not (data.get("ui") is Dictionary)) or (not E.numeric(data.ui.get("current_page")))) or E.lt(data.ui.current_page, 0)):
|
||||||
|
return "invalid ui.current_page"
|
||||||
|
if (not (data.get("state") is Dictionary)):
|
||||||
|
return "state must be a Dictionary"
|
||||||
|
var saved: Dictionary = data.state
|
||||||
|
var required = ["version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins", "binds", "amount", "distribution", "auto_resources", "auto_star_deficits", "production_remainders", "resource_remainders", "evolution_remainders", "life_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders", "civilization_stats", "civilization_homes", "ethics", "stats"]
|
||||||
|
# Optional additions to the current format. Saves predating indicators keep
|
||||||
|
# their quantities and receive neutral defaults from Owniverse on load.
|
||||||
|
for field in saved:
|
||||||
|
if (not E.contains(required, field) and not E.contains(["world_parameters", "trends"], field)):
|
||||||
|
return "unsupported state structure"
|
||||||
|
if saved.has("world_parameters"):
|
||||||
|
if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())):
|
||||||
|
return "invalid state.world_parameters"
|
||||||
|
for key in WorldParameters.defaults():
|
||||||
|
if ((not saved.world_parameters.has(key)) or E.lt(saved.world_parameters[key], 1)):
|
||||||
|
return E.add("invalid world parameter ", key)
|
||||||
|
if (E.contains(["Bur", "Exp", "Agg"], key) and E.gt(saved.world_parameters[key], 19)):
|
||||||
|
return E.add("invalid world scale ", key)
|
||||||
|
if saved.has("trends"):
|
||||||
|
if ((not _number_map(saved.trends)) or E.ne(saved.trends.size(), WorldParameters.TRENDS.size())):
|
||||||
|
return "invalid state.trends"
|
||||||
|
for key in WorldParameters.TRENDS:
|
||||||
|
if ((not saved.trends.has(key)) or E.ge(saved.trends[key], 1)):
|
||||||
|
return E.add("invalid trend ", key)
|
||||||
|
for field in required:
|
||||||
|
if (not saved.has(field)):
|
||||||
|
return E.add("missing state.", field)
|
||||||
|
if E.ne(saved.version, STATE_VERSION):
|
||||||
|
return "unsupported state.version"
|
||||||
|
if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))):
|
||||||
|
return "invalid state.time"
|
||||||
|
if E.ne(saved.time.year, SimulationTime.processed_year(saved.time.datetime)):
|
||||||
|
return "invalid processed time"
|
||||||
|
if saved.time.has("tick_remainder"):
|
||||||
|
if not E.finite(saved.time.tick_remainder) or E.ge(E.absolute(saved.time.tick_remainder), 1):
|
||||||
|
return "invalid fractional tick"
|
||||||
|
if (((not _number(saved.speed, true)) or E.lt(saved.speed, 1)) or E.gt(saved.speed, speed_limit)):
|
||||||
|
return "invalid state.speed"
|
||||||
|
if ((not _number(saved.speed_in_year)) or E.ne(saved.speed_in_year, E.power(10, E.subtract(saved.speed, 1)))):
|
||||||
|
return "invalid state.speed_in_year"
|
||||||
|
if data.has("simulation"):
|
||||||
|
var checkpoint: Variant = data.simulation
|
||||||
|
if not checkpoint is Dictionary or not checkpoint.get("productions") is Array:
|
||||||
|
return "invalid simulation checkpoint"
|
||||||
|
if not _number(checkpoint.get("speed"), true) or E.lt(checkpoint.speed, 1) or E.gt(checkpoint.speed, speed_limit):
|
||||||
|
return "invalid simulation speed"
|
||||||
|
for name in ["requested_clock", "active_clock"]:
|
||||||
|
var clock: Variant = checkpoint.get(name)
|
||||||
|
if not clock is Dictionary: return "invalid simulation clock"
|
||||||
|
if clock.is_empty(): continue
|
||||||
|
if not _number(clock.get("datetime")) or not _number(clock.get("year")):
|
||||||
|
return "invalid simulation clock"
|
||||||
|
if E.lt(clock.datetime, saved.time.datetime) or E.ne(clock.year, SimulationTime.processed_year(clock.datetime)):
|
||||||
|
return "invalid simulation clock"
|
||||||
|
if not E.finite(clock.get("tick_remainder", 0)) or E.ge(E.absolute(clock.get("tick_remainder", 0)), 1):
|
||||||
|
return "invalid simulation tick"
|
||||||
|
if not checkpoint.active_clock.is_empty():
|
||||||
|
if checkpoint.requested_clock.is_empty() or E.gt(checkpoint.active_clock.datetime, checkpoint.requested_clock.datetime):
|
||||||
|
return "invalid simulation clock order"
|
||||||
|
for command in checkpoint.productions:
|
||||||
|
if not command is Dictionary or not entities.has(command.get("id")) or not command.get("resource") is bool or not _number(command.get("amount")):
|
||||||
|
return "invalid queued production"
|
||||||
|
if command.resource and command.id not in ["S", "H"]: return "invalid queued resource production"
|
||||||
|
if ((not _number(saved.energy, true)) or (not _number_map(saved.amount, true))):
|
||||||
|
return "invalid state resources"
|
||||||
|
if ((not (saved.stats is Dictionary)) or (not _number_map(saved.stats.get("total_amount"), true))):
|
||||||
|
return "invalid state.stats.total_amount"
|
||||||
|
if E.ne(saved.amount.keys(), saved.stats.total_amount.keys()):
|
||||||
|
return "amount and total_amount keys differ"
|
||||||
|
if ((not (saved.binds is Dictionary)) or (not (saved.batches is Dictionary))):
|
||||||
|
return "state.binds and state.batches must be Dictionaries"
|
||||||
|
for owner_id in saved.binds:
|
||||||
|
if ((not saved.amount.has(owner_id)) or (not _number_map(saved.binds[owner_id], true))):
|
||||||
|
return E.add("invalid state.binds.", owner_id)
|
||||||
|
for id in saved.binds[owner_id]:
|
||||||
|
if (not saved.amount.has(id)):
|
||||||
|
return E.add("unknown bound resource ", id)
|
||||||
|
for id in saved.batches:
|
||||||
|
if ((not saved.amount.has(id)) or (not (saved.batches[id] is Array))):
|
||||||
|
return E.add("invalid state.batches.", id)
|
||||||
|
for batch in saved.batches[id]:
|
||||||
|
if ((((not (batch is Dictionary)) or (not _number(batch.get("amount"), true))) or (not _number(batch.get("start_year")))) or (not _number(batch.get("end_year")))):
|
||||||
|
return E.add("invalid batch in ", id)
|
||||||
|
if E.lt(batch.end_year, batch.start_year):
|
||||||
|
return E.add("reversed batch interval in ", id)
|
||||||
|
if E.ne(batch.has("created_from"), batch.has("created_to")):
|
||||||
|
return E.add("incomplete batch creation interval in ", id)
|
||||||
|
if (batch.has("created_from") and (((((not _number(batch.created_from)) or (not _number(batch.created_to))) or E.le(batch.created_to, batch.created_from)) or E.lt(batch.start_year, batch.created_from)))):
|
||||||
|
return E.add("invalid batch creation interval in ", id)
|
||||||
|
if (batch.has("processed_until") and ((((not _number(batch.processed_until)) or E.lt(batch.processed_until, batch.start_year)) or E.gt(batch.processed_until, batch.end_year)))):
|
||||||
|
return E.add("invalid batch.processed_until in ", id)
|
||||||
|
if (batch.has("withdraw_remainder") and (((not _number(batch.withdraw_remainder)) or E.gt(batch.withdraw_remainder, batch.amount)))):
|
||||||
|
return E.add("invalid batch.withdraw_remainder in ", id)
|
||||||
|
for field in ["batch_origins", "distribution", "production_remainders", "evolution_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "life_remainders", "civilization_remainders", "civilization_stats"]:
|
||||||
|
if (not _number_map(saved[field], false, false)):
|
||||||
|
return E.add("invalid state.", field)
|
||||||
|
if ((not _number_map(saved.auto_resources, true)) or (not _number_map(saved.auto_star_deficits, false, false))):
|
||||||
|
return "invalid automatic production state"
|
||||||
|
if ((((not (saved.resource_remainders is Dictionary)) or E.ne(saved.resource_remainders.keys().size(), 2)) or (not saved.resource_remainders.has("basic"))) or (not saved.resource_remainders.has("automatic"))):
|
||||||
|
return "invalid state.resource_remainders"
|
||||||
|
for channel in ["basic", "automatic"]:
|
||||||
|
if (not _number_map(saved.resource_remainders[channel], false, false)):
|
||||||
|
return E.add("invalid state.resource_remainders.", channel)
|
||||||
|
if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))):
|
||||||
|
return "invalid civilization state"
|
||||||
|
for id in saved.ethics:
|
||||||
|
if (not E.contains(stages, id) or (not _number_map(saved.ethics[id], true))):
|
||||||
|
return E.add("invalid state.ethics.", str(id))
|
||||||
|
for ethic in saved.ethics[id]:
|
||||||
|
if not E.contains(["m", "l", "w", "d", "c", "b"], ethic):
|
||||||
|
return E.add("invalid ethic ", str(ethic))
|
||||||
|
for id in saved.civilization_homes:
|
||||||
|
if (not _number_map(saved.civilization_homes[id], true)):
|
||||||
|
return "invalid civilization homes"
|
||||||
|
return ""
|
||||||
|
|
||||||
return data if data is Dictionary else {}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -48,7 +349,7 @@ func save_settings():
|
|||||||
func load_settings():
|
func load_settings():
|
||||||
var config = ConfigFile.new()
|
var config = ConfigFile.new()
|
||||||
var err = config.load(SAVE_PATH)
|
var err = config.load(SAVE_PATH)
|
||||||
if err != OK:
|
if E.ne(err, OK):
|
||||||
return false
|
return false
|
||||||
for item in Global.Settings:
|
for item in Global.Settings:
|
||||||
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
id|stage|span|home|stars|colonizes|fights|diplomacy|worships|ethic|progress|degradation|colonization|war|diplomacy_rate|worship|fall
|
||||||
|
C0.2:m|C0.2|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.1:1
|
||||||
|
C0.2:b|C0.2|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.1:1
|
||||||
|
C0.2:d|C0.2|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.1:1
|
||||||
|
C0.2:l|C0.2|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.1:1
|
||||||
|
C0.2:c|C0.2|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.1:1
|
||||||
|
C0.2:w|C0.2|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.1:1
|
||||||
|
C0.3:m|C0.3|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.2:1
|
||||||
|
C0.3:b|C0.3|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.2:1
|
||||||
|
C0.3:d|C0.3|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.2:1
|
||||||
|
C0.3:l|C0.3|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.2:1
|
||||||
|
C0.3:c|C0.3|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.2:1
|
||||||
|
C0.3:w|C0.3|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.2:1
|
||||||
|
C0.4:m|C0.4|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.3:1
|
||||||
|
C0.4:b|C0.4|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.3:1
|
||||||
|
C0.4:d|C0.4|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.3:1
|
||||||
|
C0.4:l|C0.4|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.3:1
|
||||||
|
C0.4:c|C0.4|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.3:1
|
||||||
|
C0.4:w|C0.4|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.3:1
|
||||||
|
C0.5:m|C0.5|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.4:1
|
||||||
|
C0.5:b|C0.5|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.4:1
|
||||||
|
C0.5:d|C0.5|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.4:1
|
||||||
|
C0.5:l|C0.5|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.4:1
|
||||||
|
C0.5:c|C0.5|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.4:1
|
||||||
|
C0.5:w|C0.5|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.4:1
|
||||||
|
C0.6:m|C0.6|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.5:1
|
||||||
|
C0.6:b|C0.6|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.5:1
|
||||||
|
C0.6:d|C0.6|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.5:1
|
||||||
|
C0.6:l|C0.6|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.5:1
|
||||||
|
C0.6:c|C0.6|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.5:1
|
||||||
|
C0.6:w|C0.6|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.5:1
|
||||||
|
C0.7:m|C0.7|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.6:1
|
||||||
|
C0.7:b|C0.7|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.6:1
|
||||||
|
C0.7:d|C0.7|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.6:1
|
||||||
|
C0.7:l|C0.7|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.6:1
|
||||||
|
C0.7:c|C0.7|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.6:1
|
||||||
|
C0.7:w|C0.7|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.6:1
|
||||||
|
C0.8:m|C0.8|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.7:1
|
||||||
|
C0.8:b|C0.8|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.7:1
|
||||||
|
C0.8:d|C0.8|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.7:1
|
||||||
|
C0.8:l|C0.8|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.7:1
|
||||||
|
C0.8:c|C0.8|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.7:1
|
||||||
|
C0.8:w|C0.8|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.7:1
|
||||||
|
C0.9:m|C0.9|2E+03||||||1|m|0.0084|0.01|0.0000625|0.001024||1.6|C0.8:512^C0.7:256^EON5:21^EON1:6
|
||||||
|
C0.9:b|C0.9|2E+03||||||1|b|0.01859375|0.02|0.00003125|0.0025||1|C0.8:512^C0.7:256^EON5:43^EON1:9
|
||||||
|
C0.9:d|C0.9|2E+03||||||1|d|0.0112|0.01538461538|0.0005|0.00015625||1|C0.8:512^C0.7:256^EON5:7^EON1:3
|
||||||
|
C0.9:l|C0.9|2E+03||||||1|l|0.0504|0.03571428571|0.0005|0.0025||0.5|C0.8:512^C0.7:256^EON5:60^EON1:11
|
||||||
|
C0.9:c|C0.9|2E+03||||||1|c|0.021|0.02604166667|0.004096|0.006103515625||0.8|C0.8:512^C0.7:256^EON5:252^EON1:19
|
||||||
|
C0.9:w|C0.9|2E+03||||||1|w|0.02625|0.03125|0.0005|0.016384||1.25|C0.8:512^C0.7:256^EON5:260^EON1:26
|
||||||
|
C1.0:m|C1.0|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C0.9:1024^EON5:21^EON1:6
|
||||||
|
C1.0:b|C1.0|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C0.9:1024^EON5:43^EON1:9
|
||||||
|
C1.0:d|C1.0|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C0.9:1024^EON5:7^EON1:3
|
||||||
|
C1.0:l|C1.0|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C0.9:1024^EON5:60^EON1:11
|
||||||
|
C1.0:c|C1.0|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C0.9:1024^EON5:152^EON1:19
|
||||||
|
C1.0:w|C1.0|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C0.9:1024^EON5:260^EON1:26
|
||||||
|
C1.1:m|C1.1|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.0:1024^C0.9:256^EON5:21^EON1:6
|
||||||
|
C1.1:b|C1.1|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.0:1024^C0.9:256^EON5:43^EON1:9
|
||||||
|
C1.1:d|C1.1|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.0:1024^C0.9:256^EON5:7^EON1:3
|
||||||
|
C1.1:l|C1.1|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.0:1024^C0.9:256^EON5:60^EON1:11
|
||||||
|
C1.1:c|C1.1|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.0:1024^C0.9:256^EON5:152^EON1:19
|
||||||
|
C1.1:w|C1.1|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.0:1024^C0.9:256^EON5:260^EON1:26
|
||||||
|
C1.2:m|C1.2|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.1:256^C1.0:1024^C0.9:256^EON5:21^EON1:6
|
||||||
|
C1.2:b|C1.2|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.1:256^C1.0:1024^C0.9:256^EON5:43^EON1:9
|
||||||
|
C1.2:d|C1.2|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.1:256^C1.0:1024^C0.9:256^EON5:7^EON1:3
|
||||||
|
C1.2:l|C1.2|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.1:256^C1.0:1024^C0.9:256^EON5:60^EON1:11
|
||||||
|
C1.2:c|C1.2|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.1:256^C1.0:1024^C0.9:256^EON5:152^EON1:19
|
||||||
|
C1.2:w|C1.2|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.1:256^C1.0:1024^C0.9:256^EON5:260^EON1:26
|
||||||
|
C1.3:m|C1.3|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.2:256^C1.0:1024^C0.9:64^EON5:13^EON1:13
|
||||||
|
C1.3:b|C1.3|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.2:256^C1.0:1024^C0.9:64^EON5:22^EON1:22
|
||||||
|
C1.3:d|C1.3|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.2:256^C1.0:1024^C0.9:64^EON5:5^EON1:5
|
||||||
|
C1.3:l|C1.3|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.2:256^C1.0:1024^C0.9:64^EON5:30^EON1:30
|
||||||
|
C1.3:c|C1.3|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.2:256^C1.0:1024^C0.9:64^EON5:64^EON1:64
|
||||||
|
C1.3:w|C1.3|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.2:256^C1.0:1024^C0.9:64^EON5:100^EON1:100
|
||||||
|
C1.4:m|C1.4|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.3:384^C1.0:896^EON5:6^EON1:21
|
||||||
|
C1.4:b|C1.4|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.3:384^C1.0:896^EON5:9^EON1:43
|
||||||
|
C1.4:d|C1.4|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.3:384^C1.0:896^EON5:3^EON1:7
|
||||||
|
C1.4:l|C1.4|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.3:384^C1.0:896^EON5:11^EON1:60
|
||||||
|
C1.4:c|C1.4|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.3:384^C1.0:896^EON5:19^EON1:152
|
||||||
|
C1.4:w|C1.4|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.3:384^C1.0:896^EON5:26^EON1:260
|
||||||
|
C1.5:m|C1.5|5E+03||||||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.4:512^C1.0:768^EON5:6^EON1:21
|
||||||
|
C1.5:b|C1.5|5E+03||||||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.4:512^C1.0:768^EON5:9^EON1:43
|
||||||
|
C1.5:d|C1.5|5E+03||||||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.4:512^C1.0:768^EON5:3^EON1:7
|
||||||
|
C1.5:l|C1.5|5E+03||||||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.4:512^C1.0:768^EON5:11^EON1:60
|
||||||
|
C1.5:c|C1.5|5E+03||||||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.4:512^C1.0:768^EON5:19^EON1:152
|
||||||
|
C1.5:w|C1.5|5E+03||||||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.4:512^C1.0:768^EON5:26^EON1:260
|
||||||
|
C1.6:m|C1.6|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.5:768^C1.0:512^EON5:6^RP:21
|
||||||
|
C1.6:b|C1.6|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.5:768^C1.0:512^EON5:9^RP:43
|
||||||
|
C1.6:d|C1.6|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.5:768^C1.0:512^EON5:3^RP:7
|
||||||
|
C1.6:l|C1.6|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.5:768^C1.0:512^EON5:11^RP:60
|
||||||
|
C1.6:c|C1.6|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.5:768^C1.0:512^EON5:19^RP:152
|
||||||
|
C1.6:w|C1.6|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.5:768^C1.0:512^EON5:26^RP:260
|
||||||
|
C1.7:m|C1.7|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.6:896^C1.0:384^EON5:6^RP:21
|
||||||
|
C1.7:b|C1.7|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.6:896^C1.0:384^EON5:9^RP:43
|
||||||
|
C1.7:d|C1.7|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.6:896^C1.0:384^EON5:3^RP:7
|
||||||
|
C1.7:l|C1.7|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.6:896^C1.0:384^EON5:11^RP:60
|
||||||
|
C1.7:c|C1.7|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.6:896^C1.0:384^EON5:19^RP:152
|
||||||
|
C1.7:w|C1.7|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.6:896^C1.0:384^EON5:26^RP:260
|
||||||
|
C1.8:m|C1.8|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.7:1024^C1.0:256^EON5:6^RP:21
|
||||||
|
C1.8:b|C1.8|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.7:1024^C1.0:256^EON5:9^RP:43
|
||||||
|
C1.8:d|C1.8|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.7:1024^C1.0:256^EON5:3^RP:7
|
||||||
|
C1.8:l|C1.8|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.7:1024^C1.0:256^EON5:11^RP:60
|
||||||
|
C1.8:c|C1.8|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.7:1024^C1.0:256^EON5:19^RP:152
|
||||||
|
C1.8:w|C1.8|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.7:1024^C1.0:256^EON5:26^RP:260
|
||||||
|
C1.9:m|C1.9|5E+03|||1|||1|m|0.009|0.01|0.0000625|0.001024||1.6|C1.8:1024^C1.0:128^EON5:6^RP:21
|
||||||
|
C1.9:b|C1.9|5E+03|||1|||1|b|0.019921875|0.02|0.00003125|0.0025||1|C1.8:1024^C1.0:128^EON5:9^RP:43
|
||||||
|
C1.9:d|C1.9|5E+03|||1|||1|d|0.012|0.01538461538|0.0005|0.00015625||1|C1.8:1024^C1.0:128^EON5:3^RP:7
|
||||||
|
C1.9:l|C1.9|5E+03|||1|||1|l|0.054|0.03571428571|0.0005|0.0025||0.5|C1.8:1024^C1.0:128^EON5:11^RP:60
|
||||||
|
C1.9:c|C1.9|5E+03|||1|||1|c|0.0225|0.02604166667|0.004096|0.006103515625||0.8|C1.8:1024^C1.0:128^EON5:19^RP:152
|
||||||
|
C1.9:w|C1.9|5E+03|||1|||1|w|0.028125|0.03125|0.0005|0.016384||1.25|C1.8:1024^C1.0:128^EON5:26^RP:260
|
||||||
|
C2.0:m|C2.0|5E+03|DwStar|1E+00|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C1.9:1024^C1.0:128^RP:28
|
||||||
|
C2.0:b|C2.0|5E+03|DwStar|1E+00|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C1.9:1024^C1.0:128^EON5:9^RP:59
|
||||||
|
C2.0:d|C2.0|5E+03|DwStar|1E+00|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C1.9:1024^C1.0:128^EON5:3^RP:8
|
||||||
|
C2.0:l|C2.0|5E+03|DwStar|1E+00|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C1.9:1024^C1.0:128^EON5:11^RP:85
|
||||||
|
C2.0:c|C2.0|5E+03|DwStar|1E+00|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C1.9:1024^C1.0:128^EON5:19^RP:235
|
||||||
|
C2.0:w|C2.0|5E+03|DwStar|1E+00|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C1.9:1024^C1.0:128^EON5:26^RP:419
|
||||||
|
C2.1:m|C2.1|5E+03|DwStar|1E+01|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.0:1
|
||||||
|
C2.1:b|C2.1|5E+03|DwStar|1E+01|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.0:1
|
||||||
|
C2.1:d|C2.1|5E+03|DwStar|1E+01|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.0:1
|
||||||
|
C2.1:l|C2.1|5E+03|DwStar|1E+01|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.0:1
|
||||||
|
C2.1:c|C2.1|5E+03|DwStar|1E+01|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.0:1
|
||||||
|
C2.1:w|C2.1|5E+03|DwStar|1E+01|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.0:1
|
||||||
|
C2.2:m|C2.2|1E+04|DwStar|1E+02|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.1:1024^C2.0:6
|
||||||
|
C2.2:b|C2.2|1E+04|DwStar|1E+02|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.1:1024^C2.0:9
|
||||||
|
C2.2:d|C2.2|1E+04|DwStar|1E+02|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.1:1024^C2.0:3
|
||||||
|
C2.2:l|C2.2|1E+04|DwStar|1E+02|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.1:1024^C2.0:11
|
||||||
|
C2.2:c|C2.2|1E+04|DwStar|1E+02|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.1:1024^C2.0:19
|
||||||
|
C2.2:w|C2.2|1E+04|DwStar|1E+02|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.1:1024^C2.0:26
|
||||||
|
C2.3:m|C2.3|2E+04|DwStar|1E+03|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.2:1024^C2.1:6
|
||||||
|
C2.3:b|C2.3|2E+04|DwStar|1E+03|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.2:1024^C2.1:9
|
||||||
|
C2.3:d|C2.3|2E+04|DwStar|1E+03|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.2:1024^C2.1:3
|
||||||
|
C2.3:l|C2.3|2E+04|DwStar|1E+03|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.2:1024^C2.1:11
|
||||||
|
C2.3:c|C2.3|2E+04|DwStar|1E+03|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.2:1024^C2.1:19
|
||||||
|
C2.3:w|C2.3|2E+04|DwStar|1E+03|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.2:1024^C2.1:26
|
||||||
|
C2.4:m|C2.4|2E+04|DwStar|1E+04|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.3:1024^C2.2:6
|
||||||
|
C2.4:b|C2.4|2E+04|DwStar|1E+04|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.3:1024^C2.2:9
|
||||||
|
C2.4:d|C2.4|2E+04|DwStar|1E+04|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.3:1024^C2.2:3
|
||||||
|
C2.4:l|C2.4|2E+04|DwStar|1E+04|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.3:1024^C2.2:11
|
||||||
|
C2.4:c|C2.4|2E+04|DwStar|1E+04|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.3:1024^C2.2:19
|
||||||
|
C2.4:w|C2.4|2E+04|DwStar|1E+04|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.3:1024^C2.2:26
|
||||||
|
C2.5:m|C2.5|3E+04|DwStar|1E+05|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.4:1024^C2.3:6
|
||||||
|
C2.5:b|C2.5|3E+04|DwStar|1E+05|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.4:1024^C2.3:9
|
||||||
|
C2.5:d|C2.5|3E+04|DwStar|1E+05|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.4:1024^C2.3:3
|
||||||
|
C2.5:l|C2.5|3E+04|DwStar|1E+05|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.4:1024^C2.3:11
|
||||||
|
C2.5:c|C2.5|3E+04|DwStar|1E+05|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.4:1024^C2.3:19
|
||||||
|
C2.5:w|C2.5|3E+04|DwStar|1E+05|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.4:1024^C2.3:26
|
||||||
|
C2.6:m|C2.6|3E+04|DwStar|1E+06|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.5:1024^C2.4:6
|
||||||
|
C2.6:b|C2.6|3E+04|DwStar|1E+06|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.5:1024^C2.4:9
|
||||||
|
C2.6:d|C2.6|3E+04|DwStar|1E+06|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.5:1024^C2.4:3
|
||||||
|
C2.6:l|C2.6|3E+04|DwStar|1E+06|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.5:1024^C2.4:11
|
||||||
|
C2.6:c|C2.6|3E+04|DwStar|1E+06|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.5:1024^C2.4:19
|
||||||
|
C2.6:w|C2.6|3E+04|DwStar|1E+06|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.5:1024^C2.4:26
|
||||||
|
C2.7:m|C2.7|4E+04|GiStar|1E+07|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.6:1024^C2.5:6
|
||||||
|
C2.7:b|C2.7|4E+04|GiStar|1E+07|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.6:1024^C2.5:9
|
||||||
|
C2.7:d|C2.7|4E+04|GiStar|1E+07|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.6:1024^C2.5:3
|
||||||
|
C2.7:l|C2.7|4E+04|GiStar|1E+07|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.6:1024^C2.5:11
|
||||||
|
C2.7:c|C2.7|4E+04|GiStar|1E+07|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.6:1024^C2.5:19
|
||||||
|
C2.7:w|C2.7|4E+04|GiStar|1E+07|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.6:1024^C2.5:26
|
||||||
|
C2.8:m|C2.8|4E+04|GiStar|1E+08|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.7:1024^C2.6:6
|
||||||
|
C2.8:b|C2.8|4E+04|GiStar|1E+08|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.7:1024^C2.6:9
|
||||||
|
C2.8:d|C2.8|4E+04|GiStar|1E+08|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.7:1024^C2.6:3
|
||||||
|
C2.8:l|C2.8|4E+04|GiStar|1E+08|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.7:1024^C2.6:11
|
||||||
|
C2.8:c|C2.8|4E+04|GiStar|1E+08|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.7:1024^C2.6:19
|
||||||
|
C2.8:w|C2.8|4E+04|GiStar|1E+08|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.7:1024^C2.6:26
|
||||||
|
C2.9:m|C2.9|5E+04|GiStar|1E+09|1|1|1|1|m|0.006|0.01|0.0005|0.00128|1|1.6|C2.8:1024^C2.7:6
|
||||||
|
C2.9:b|C2.9|5E+04|GiStar|1E+09|1|1|1|1|b|0.02125|0.015625|0.00003125|0.0025|0.64|1|C2.8:1024^C2.7:9
|
||||||
|
C2.9:d|C2.9|5E+04|GiStar|1E+09|1|1|1|1|d|0.0128|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.8:1024^C2.7:3
|
||||||
|
C2.9:l|C2.9|5E+04|GiStar|1E+09|1|1|1|1|l|0.0576|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.8:1024^C2.7:11
|
||||||
|
C2.9:c|C2.9|5E+04|GiStar|1E+09|1|1|1|1|c|0.024|0.04166666667|0.004096|0.006103515625|1|0.8|C2.8:1024^C2.7:19
|
||||||
|
C2.9:w|C2.9|5E+04|GiStar|1E+09|1|1|1|1|w|0.03|0.025|0.000256|0.03072|0.25|1.25|C2.8:1024^C2.7:26
|
||||||
|
C3.0:m|C3.0|1E+05|GiStar|1E+10|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C2.9:1024^C2.8:6
|
||||||
|
C3.0:b|C3.0|1E+05|GiStar|1E+10|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C2.9:1024^C2.8:9
|
||||||
|
C3.0:d|C3.0|1E+05|GiStar|1E+10|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C2.9:1024^C2.8:3
|
||||||
|
C3.0:l|C3.0|1E+05|GiStar|1E+10|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C2.9:1024^C2.8:11
|
||||||
|
C3.0:c|C3.0|1E+05|GiStar|1E+10|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C2.9:1024^C2.8:19
|
||||||
|
C3.0:w|C3.0|1E+05|GiStar|1E+10|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C2.9:1024^C2.8:26
|
||||||
|
C3.1:m|C3.1|1E+05|GiStar|1E+11|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.0:1024^C2.9:6
|
||||||
|
C3.1:b|C3.1|1E+05|GiStar|1E+11|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.0:1024^C2.9:9
|
||||||
|
C3.1:d|C3.1|1E+05|GiStar|1E+11|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.0:1024^C2.9:3
|
||||||
|
C3.1:l|C3.1|1E+05|GiStar|1E+11|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.0:1024^C2.9:11
|
||||||
|
C3.1:c|C3.1|1E+05|GiStar|1E+11|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.0:1024^C2.9:19
|
||||||
|
C3.1:w|C3.1|1E+05|GiStar|1E+11|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.0:1024^C2.9:26
|
||||||
|
C3.2:m|C3.2|2E+05|WD|1E+12|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.1:1024^C3.0:6
|
||||||
|
C3.2:b|C3.2|2E+05|WD|1E+12|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.1:1024^C3.0:9
|
||||||
|
C3.2:d|C3.2|2E+05|WD|1E+12|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.1:1024^C3.0:3
|
||||||
|
C3.2:l|C3.2|2E+05|WD|1E+12|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.1:1024^C3.0:11
|
||||||
|
C3.2:c|C3.2|2E+05|WD|1E+12|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.1:1024^C3.0:19
|
||||||
|
C3.2:w|C3.2|2E+05|WD|1E+12|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.1:1024^C3.0:26
|
||||||
|
C3.3:m|C3.3|3E+05|WD|1E+13|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.2:1024^C3.1:6
|
||||||
|
C3.3:b|C3.3|3E+05|WD|1E+13|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.2:1024^C3.1:9
|
||||||
|
C3.3:d|C3.3|3E+05|WD|1E+13|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.2:1024^C3.1:3
|
||||||
|
C3.3:l|C3.3|3E+05|WD|1E+13|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.2:1024^C3.1:11
|
||||||
|
C3.3:c|C3.3|3E+05|WD|1E+13|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.2:1024^C3.1:19
|
||||||
|
C3.3:w|C3.3|3E+05|WD|1E+13|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.2:1024^C3.1:26
|
||||||
|
C3.4:m|C3.4|4E+05|WD|1E+14|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.3:1024^C3.2:6
|
||||||
|
C3.4:b|C3.4|4E+05|WD|1E+14|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.3:1024^C3.2:9
|
||||||
|
C3.4:d|C3.4|4E+05|WD|1E+14|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.3:1024^C3.2:3
|
||||||
|
C3.4:l|C3.4|4E+05|WD|1E+14|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.3:1024^C3.2:11
|
||||||
|
C3.4:c|C3.4|4E+05|WD|1E+14|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.3:1024^C3.2:19
|
||||||
|
C3.4:w|C3.4|4E+05|WD|1E+14|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.3:1024^C3.2:26
|
||||||
|
C3.5:m|C3.5|5E+05|NS|1E+15|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.4:1024^C3.3:6
|
||||||
|
C3.5:b|C3.5|5E+05|NS|1E+15|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.4:1024^C3.3:9
|
||||||
|
C3.5:d|C3.5|5E+05|NS|1E+15|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.4:1024^C3.3:3
|
||||||
|
C3.5:l|C3.5|5E+05|NS|1E+15|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.4:1024^C3.3:11
|
||||||
|
C3.5:c|C3.5|5E+05|NS|1E+15|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.4:1024^C3.3:19
|
||||||
|
C3.5:w|C3.5|5E+05|NS|1E+15|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.4:1024^C3.3:26
|
||||||
|
C3.6:m|C3.6|6E+05|NS|1E+16|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.5:1024^C3.4:6
|
||||||
|
C3.6:b|C3.6|6E+05|NS|1E+16|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.5:1024^C3.4:9
|
||||||
|
C3.6:d|C3.6|6E+05|NS|1E+16|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.5:1024^C3.4:3
|
||||||
|
C3.6:l|C3.6|6E+05|NS|1E+16|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.5:1024^C3.4:11
|
||||||
|
C3.6:c|C3.6|6E+05|NS|1E+16|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.5:1024^C3.4:19
|
||||||
|
C3.6:w|C3.6|6E+05|NS|1E+16|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.5:1024^C3.4:26
|
||||||
|
C3.7:m|C3.7|7E+05|MGT|1E+17|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.6:1024^C3.5:6
|
||||||
|
C3.7:b|C3.7|7E+05|MGT|1E+17|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.6:1024^C3.5:9
|
||||||
|
C3.7:d|C3.7|7E+05|MGT|1E+17|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.6:1024^C3.5:3
|
||||||
|
C3.7:l|C3.7|7E+05|MGT|1E+17|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.6:1024^C3.5:11
|
||||||
|
C3.7:c|C3.7|7E+05|MGT|1E+17|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.6:1024^C3.5:19
|
||||||
|
C3.7:w|C3.7|7E+05|MGT|1E+17|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.6:1024^C3.5:26
|
||||||
|
C3.8:m|C3.8|8E+05|MGT|1E+18|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.7:1024^C3.6:6
|
||||||
|
C3.8:b|C3.8|8E+05|MGT|1E+18|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.7:1024^C3.6:9
|
||||||
|
C3.8:d|C3.8|8E+05|MGT|1E+18|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.7:1024^C3.6:3
|
||||||
|
C3.8:l|C3.8|8E+05|MGT|1E+18|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.7:1024^C3.6:11
|
||||||
|
C3.8:c|C3.8|8E+05|MGT|1E+18|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.7:1024^C3.6:19
|
||||||
|
C3.8:w|C3.8|8E+05|MGT|1E+18|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.7:1024^C3.6:26
|
||||||
|
C3.9:m|C3.9|9E+05|BH|1E+19|1|1|1|1|m|0.006375|0.01|0.0005|0.00128|1|1.6|C3.8:1024^C3.7:6
|
||||||
|
C3.9:b|C3.9|9E+05|BH|1E+19|1|1|1|1|b|0.022578125|0.015625|0.00003125|0.0025|0.64|1|C3.8:1024^C3.7:9
|
||||||
|
C3.9:d|C3.9|9E+05|BH|1E+19|1|1|1|1|d|0.0136|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.8:1024^C3.7:3
|
||||||
|
C3.9:l|C3.9|9E+05|BH|1E+19|1|1|1|1|l|0.0612|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.8:1024^C3.7:11
|
||||||
|
C3.9:c|C3.9|9E+05|BH|1E+19|1|1|1|1|c|0.0255|0.04166666667|0.004096|0.006103515625|1|0.8|C3.8:1024^C3.7:19
|
||||||
|
C3.9:w|C3.9|9E+05|BH|1E+19|1|1|1|1|w|0.031875|0.025|0.000256|0.03072|0.25|1.25|C3.8:1024^C3.7:26
|
||||||
|
C4.0:m|C4.0|2E+06|BH|1E+20|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C3.9:1024^C3.8:6
|
||||||
|
C4.0:b|C4.0|2E+06|BH|1E+20|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C3.9:1024^C3.8:9
|
||||||
|
C4.0:d|C4.0|2E+06|BH|1E+20|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C3.9:1024^C3.8:3
|
||||||
|
C4.0:l|C4.0|2E+06|BH|1E+20|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C3.9:1024^C3.8:11
|
||||||
|
C4.0:c|C4.0|2E+06|BH|1E+20|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C3.9:1024^C3.8:19
|
||||||
|
C4.0:w|C4.0|2E+06|BH|1E+20|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C3.9:1024^C3.8:26
|
||||||
|
C4.1:m|C4.1|2E+06|BH|1E+21|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.0:1024^C3.9:6
|
||||||
|
C4.1:b|C4.1|2E+06|BH|1E+21|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.0:1024^C3.9:9
|
||||||
|
C4.1:d|C4.1|2E+06|BH|1E+21|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.0:1024^C3.9:3
|
||||||
|
C4.1:l|C4.1|2E+06|BH|1E+21|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.0:1024^C3.9:11
|
||||||
|
C4.1:c|C4.1|2E+06|BH|1E+21|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.0:1024^C3.9:19
|
||||||
|
C4.1:w|C4.1|2E+06|BH|1E+21|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.0:1024^C3.9:26
|
||||||
|
C4.2:m|C4.2|4E+06|SMBH|1E+22|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.1:1024^C4.0:6
|
||||||
|
C4.2:b|C4.2|4E+06|SMBH|1E+22|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.1:1024^C4.0:9
|
||||||
|
C4.2:d|C4.2|4E+06|SMBH|1E+22|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.1:1024^C4.0:3
|
||||||
|
C4.2:l|C4.2|4E+06|SMBH|1E+22|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.1:1024^C4.0:11
|
||||||
|
C4.2:c|C4.2|4E+06|SMBH|1E+22|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.1:1024^C4.0:19
|
||||||
|
C4.2:w|C4.2|4E+06|SMBH|1E+22|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.1:1024^C4.0:26
|
||||||
|
C4.3:m|C4.3|6E+06|SMBH|1E+23|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.2:1024^C4.1:6
|
||||||
|
C4.3:b|C4.3|6E+06|SMBH|1E+23|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.2:1024^C4.1:9
|
||||||
|
C4.3:d|C4.3|6E+06|SMBH|1E+23|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.2:1024^C4.1:3
|
||||||
|
C4.3:l|C4.3|6E+06|SMBH|1E+23|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.2:1024^C4.1:11
|
||||||
|
C4.3:c|C4.3|6E+06|SMBH|1E+23|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.2:1024^C4.1:19
|
||||||
|
C4.3:w|C4.3|6E+06|SMBH|1E+23|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.2:1024^C4.1:26
|
||||||
|
C4.4:m|C4.4|8E+06|SMBH|1E+24|1|1|1|1|m|0.00675|0.01|0.0005|0.00128|1|1.6|C4.3:1024^C4.2:6
|
||||||
|
C4.4:b|C4.4|8E+06|SMBH|1E+24|1|1|1|1|b|0.02390625|0.015625|0.00003125|0.0025|0.64|1|C4.3:1024^C4.2:9
|
||||||
|
C4.4:d|C4.4|8E+06|SMBH|1E+24|1|1|1|1|d|0.0144|0.01923076923|0.0009765625|0.0003125|2.56|1|C4.3:1024^C4.2:3
|
||||||
|
C4.4:l|C4.4|8E+06|SMBH|1E+24|1|1|1|1|l|0.0648|0.04464285714|0.0005|0.0025|1.5625|0.5|C4.3:1024^C4.2:11
|
||||||
|
C4.4:c|C4.4|8E+06|SMBH|1E+24|1|1|1|1|c|0.027|0.04166666667|0.004096|0.006103515625|1|0.8|C4.3:1024^C4.2:19
|
||||||
|
C4.4:w|C4.4|8E+06|SMBH|1E+24|1|1|1|1|w|0.03375|0.025|0.000256|0.03072|0.25|1.25|C4.3:1024^C4.2:26
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
class_name CSVLoader
|
||||||
|
|
||||||
|
const SPLITTER = '|'
|
||||||
|
|
||||||
|
static func load(csv_text: String) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
|
||||||
|
var lines := csv_text.strip_edges().split("\n")
|
||||||
|
|
||||||
|
if lines.size() < 2:
|
||||||
|
return result
|
||||||
|
|
||||||
|
var headers := lines[0].split(SPLITTER)
|
||||||
|
|
||||||
|
for i in range(1, lines.size()):
|
||||||
|
var line := lines[i].strip_edges()
|
||||||
|
|
||||||
|
if line.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var values := line.split(SPLITTER)
|
||||||
|
|
||||||
|
if values.size() != headers.size():
|
||||||
|
push_warning(
|
||||||
|
"CSV row %d has %d columns, expected %d"
|
||||||
|
% [i + 1, values.size(), headers.size()]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var elem: Dictionary = {}
|
||||||
|
|
||||||
|
for j in range(headers.size()):
|
||||||
|
elem[headers[j].strip_edges()] = values[j].strip_edges()
|
||||||
|
|
||||||
|
result[elem[headers[0]]] = elem
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bfe1fyutcw0cx
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
id|span|progress|degradation|next|fall
|
||||||
|
HP|5E+06|0.00025|0.0025|EON1|RP:1
|
||||||
|
EON1|1E+07|0.00025|0.0025|EON2|HP:100^RP:1
|
||||||
|
EON2|1E+07|0.00025|0.0025|EON3|EON1:1
|
||||||
|
EON3|1E+06|0.00025|0.0025|EON4|EON2:1000^EON1:1
|
||||||
|
EON4|1E+06|0.00025|0.0025|EON5|EON3:1000^EON1:1
|
||||||
|
EON5|5E+05|0.00025|0.0025|C0.0|EON4:2000^EON1:1
|
||||||
|
C0.0|2E+03|0.0175|0.025|C0.1|EON5:1
|
||||||
|
C0.1|2E+03|0.0175|0.025|C0.2|C0.0:1
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="keep"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="keep"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
id|is_visible|unlock|cost|bind|mass|space|lifespan|auto|gamma|blast|life_product|voids|output|destroy_level|degradation|planets|count_as|group_as|event_group|blastability|space_bonus|civ_bonus|claim
|
id|is_visible|unlock|cost|bind|mass|space|lifespan|auto|gamma|blast|life_product|voids|output|destroy_level|degradation|planets|count_as|group_as|event_group|blastability|space_bonus|civ_bonus|claim
|
||||||
S|1|S:0|S:0||||||||||||||||S||||
|
S|1|S:0|E:1||||||||||||||||S||||
|
||||||
H|1|H:0|H:0||||||||||||||||H||||
|
H|1|H:0|E:1||||||||||||||||H||||
|
||||||
He|1|He:1|||||||||||||||||||||
|
He|1|He:1|||||||||||||||||||||
|
||||||
Met|1|Met:1|||||||||||||||||||||
|
Met|1|Met:1|||||||||||||||||||||
|
||||||
BD|1|BD:1|||||||||||||||||||||
|
BD|1|BD:1|||||||||||||||||||||
|
||||||
@@ -21,14 +21,14 @@ LRG|0||||50||50000000||||S:300||DSN^SGRB%0.1|2|||RG|Star|||||
|
|||||||
HRG|0||||100||10000000||||S:400.||SN^GRB%1|2|||RG|Star|||||
|
HRG|0||||100||10000000||||S:400.||SN^GRB%1|2|||RG|Star|||||
|
||||||
RSG|0||||300||1000000||||S:500||BSN^LGRB%3|2|||RG|Star|||||
|
RSG|0||||300||1000000||||S:500||BSN^LGRB%3|2|||RG|Star|||||
|
||||||
RHG|0||||1000||100000||||S:600||HN^HGRB%10|2|||RG|Star|||||
|
RHG|0||||1000||100000||||S:600||HN^HGRB%10|2|||RG|Star|||||
|
||||||
DSN|0||||||1|||2|S:59.1||SNS:1&H:6.5&He:13.5&Met:11&blast:2|2|mass:15&H:6.5&HE:13.5&MET:11||||SNGRBB||||
|
DSN|0||||||1|||2|S:59.1||SNS:1&H:6.5&He:13.5&Met:11|2|mass:15&H:6.5&HE:13.5&MET:11||||SNGRBB||||
|
||||||
SN|0||||||1|||9|S:99.1||LNS:1&H:8&He:50&Met:11&blast:9|2|mass:18&H:8&HE:50&MET:11||||SNGRBB||||
|
SN|0||||||1|||9|S:99.1||LNS:1&H:8&He:50&Met:11|2|mass:18&H:8&HE:50&MET:11||||SNGRBB||||
|
||||||
BSN|0||||||1||.|305|S:309.1||MGT:1&H:15&He:186&Met:15&blast:305|2|mass:21&H:15&HE:186&MET:15||||SNGRBB||||
|
BSN|0||||||1||.|305|S:309.1||MGT:1&H:15&He:186&Met:15|2|mass:21&H:15&HE:186&MET:15||||SNGRBB||||
|
||||||
HN|0||||||1|||9990|S:939.1||BH40:1&H:30&He:560&Met:30&blast:9990|2|mass:40&H:30&HE:560&MET:30||||SNGRBB||||
|
HN|0||||||1|||9990|S:939.1||BH40:1&H:30&He:560&Met:30|2|mass:40&H:30&HE:560&MET:30||||SNGRBB||||
|
||||||
SGRB|0||||||1||0.001|31|S:149.1||BH25:1&H:0.5&He:10&Met:4&blast:31|2|mass:25&H:0.5&HE:10&MET:4||||SNGRBB||||
|
SGRB|0||||||1||0.001|31|S:149.1||BH25:1&H:0.5&He:10&Met:4|2|mass:25&H:0.5&HE:10&MET:4||||SNGRBB||||
|
||||||
GRB|0||||||1||0.01|136|S:239.1||BH30:1&H:19&He:12&Met:5&blast:136|2|mass:30&H:19&HE:12&MET:5||||SNGRBB||||
|
GRB|0||||||1||0.01|136|S:239.1||BH30:1&H:19&He:12&Met:5|2|mass:30&H:19&HE:12&MET:5||||SNGRBB||||
|
||||||
LGRB|0||||||1||0.03|3446|S:669.1||BH40:1&H:68&He:18&Met:9&blast:3446|2|mass:40&H:68&HE:18&MET:9||||SNGRBB||||
|
LGRB|0||||||1||0.03|3446|S:669.1||BH40:1&H:68&He:18&Met:9|2|mass:40&H:68&HE:18&MET:9||||SNGRBB||||
|
||||||
HGRB|0||||||1||0.1|97342|S:1939.1||BH60:1&H:0&He:40&Met:10&blast:97342|2|mass:60&H:0&HE:40&MET:10||||SNGRBB||||
|
HGRB|0||||||1||0.1|97342|S:1939.1||BH60:1&H:0&He:40&Met:10|2|mass:60&H:0&HE:40&MET:10||||SNGRBB||||
|
||||||
CBD|0||||0.1||||||||||||BD||||||
|
CBD|0||||0.1||||||||||||BD||||||
|
||||||
WD|1|WD:1|||||||||||||||||||||
|
WD|1|WD:1|||||||||||||||||||||
|
||||||
TWD|0||||0.8||||||||||||WD|Star|Degen||||
|
TWD|0||||0.8||||||||||||WD|Star|Degen||||
|
||||||
@@ -51,25 +51,25 @@ STN|1|G:1|H:200&G:1||211|300|30000000|0.5|||H:3000||H:211&S:320|||||CL|CL||||
|
|||||||
MC|1|G:1&S:1300|H:2000&G:1||2011|4000|300000000||||H:90000.||H:2011&S:4020|||||CL|CL||||
|
MC|1|G:1&S:1300|H:2000&G:1||2011|4000|300000000||||H:90000.||H:2011&S:4020|||||CL|CL||||
|
||||||
ASTN|1|MC:1&S:60000|H:200000&MC:1||202011|200000|100000000|0.65|||H:2000000||H:202011&S:204020|||||CL|CL||||
|
ASTN|1|MC:1&S:60000|H:200000&MC:1||202011|200000|100000000|0.65|||H:2000000||H:202011&S:204020|||||CL|CL||||
|
||||||
SCD|1|Star:300||Star:1000|||10000000|||||||||||SC|SC||0.1|4|C2.3
|
SCD|1|Star:300||Star:1000|||10000000|||||||||||SC|SC||0.1|4|C2.3
|
||||||
SCS|1|SCD:1&Star:2000||Star:10000&BH:10|||1000000000|||||||||||SC|SC||0.2|44350|C2.4
|
SCS|1|SCD:1&Star:2000||Star:10000&BH:10|||1000000000|||||||||||SC|SC||0.2|3.6|C2.4
|
||||||
SCM|1|SCS:1&Star:20000||Star:100000&BH:100|||3000000000|||||||||||SC|SC||0.3|44289|C2.5
|
SCM|1|SCS:1&Star:20000||Star:100000&BH:100|||3000000000|||||||||||SC|SC||0.3|3.4|C2.5
|
||||||
SCL|1|SCM:1&Star:200000||Star:1000000&BH:1000|||10000000000|||||||||||SC|SC||0.5|44230|C2.6
|
SCL|1|SCM:1&Star:200000||Star:1000000&BH:1000|||10000000000|||||||||||SC|SC||0.5|3.2|C2.6
|
||||||
SCG|1|SCL:1&Star:2000000||Star:10000000&BH:10000|||30000000000|||||||||||SC|SC||0.8|3|C2.7
|
SCG|1|SCL:1&Star:2000000||Star:10000000&BH:10000|||30000000000|||||||||||SC|SC||0.8|3|C2.7
|
||||||
GED|1|SMBH:1&SC:1|SMBH:10000|Star:1000000000&BH:1000000&SC:100|||12000000000|||||||||||GAL|GALE||1|44440|C2.9
|
GED|1|SMBH:1&SC:1|SMBH:10000|Star:1000000000&BH:1000000&SC:100|||12000000000|||||||||||GAL|GALE||1|1.9|C2.9
|
||||||
GES|1|GED:1&Star:2000000000|SMBH:100000|Star:10000000000&BH:10000000&SC:1000|||13000000000|||||||||||GAL|GALE||1.5|44409|C3.0
|
GES|1|GED:1&Star:2000000000|SMBH:100000|Star:10000000000&BH:10000000&SC:1000|||13000000000|||||||||||GAL|GALE||1.5|1.8|C3.0
|
||||||
GEM|1|GES:1&Star:20000000000|SMBH:1000000|Star:100000000000&BH:100000000&SC:10000|||15000000000|||||||||||GAL|GALE||2|44378|C3.1
|
GEM|1|GES:1&Star:20000000000|SMBH:1000000|Star:100000000000&BH:100000000&SC:10000|||15000000000|||||||||||GAL|GALE||2|1.7|C3.1
|
||||||
GEL|1|GEM:1&Star:200000000000|SMBH:10000000|Star:1000000000000&BH:1000000000&SC:100000|||17000000000|||||||||||GAL|GALE||3|44348|C3.2
|
GEL|1|GEM:1&Star:200000000000|SMBH:10000000|Star:1000000000000&BH:1000000000&SC:100000|||17000000000|||||||||||GAL|GALE||3|1.6|C3.2
|
||||||
GEG|1|GEL:1&Star:2000000000000|SMBH:100000000|Star:10000000000000&BH:10000000000&SC:1000000|||20000000000|||||||||||GAL|GALE||4.5|44317|C3.3
|
GEG|1|GEL:1&Star:2000000000000|SMBH:100000000|Star:10000000000000&BH:10000000000&SC:1000000|||20000000000|||||||||||GAL|GALE||4.5|1.5|C3.3
|
||||||
GSD|1|GED:1|STN:10000&ASTN:10|GED:1||-5240200|2000000000|0.8|||H:2400000000||H:4112110|||||GAL|GALS|||44318|C2.9
|
GSD|1|GED:1|STN:10000&ASTN:10|GED:1||-5240200|2000000000|0.8|||H:2400000000||H:4112110|||||GAL|GALS|||2.5|C2.9
|
||||||
GSS|1|GES:1|STN:100000&ASTN:100|GES:1||-52402000|3000000000|0.8|||H:36000000000||H:41121100|||||GAL|GALS|||44257|C3.0
|
GSS|1|GES:1|STN:100000&ASTN:100|GES:1||-52402000|3000000000|0.8|||H:36000000000||H:41121100|||||GAL|GALS|||2.3|C3.0
|
||||||
GSM|1|GEM:1|STN:1000000&ASTN:1000|GEM:1||-524020000|4000000000|0.8|||H:480000000000||H:411211000|||||GAL|GALS|||44229|C3.1
|
GSM|1|GEM:1|STN:1000000&ASTN:1000|GEM:1||-524020000|4000000000|0.8|||H:480000000000||H:411211000|||||GAL|GALS|||2.2|C3.1
|
||||||
GSL|1|GEL:1|STN:10000000&ASTN:10000|GEL:1||-5240200000|6000000000|0.8|||H:7200000000000||H:4112110000|||||GAL|GALS|||44198|C3.2
|
GSL|1|GEL:1|STN:10000000&ASTN:10000|GEL:1||-5240200000|6000000000|0.8|||H:7200000000000||H:4112110000|||||GAL|GALS|||2.1|C3.2
|
||||||
GSG|1|GEG:1|STN:100000000&ASTN:100000|GEG:1||-52402000000|9000000000|0.8|||H:108000000000000||H:41121100000|||||GAL|GALS|||2|C3.3
|
GSG|1|GEG:1|STN:100000000&ASTN:100000|GEG:1||-52402000000|9000000000|0.8|||H:108000000000000||H:41121100000|||||GAL|GALS|||2|C3.3
|
||||||
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||44317|C3.4
|
LGG|1|GAL:20||GAL:50|||10000000000|||||||||||LS|LS|||1.5|C3.4
|
||||||
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||44287|C3.6
|
LSC|1|LGG:1&GAL:100||LGG:200|||20000000000|||||||||||LS|LS|||1.4|C3.6
|
||||||
LGF|1|LSC:1&LGG:400||LSC:100|||30000000000|||||5||||||LS|LS|||44256|C3.8
|
LGF|1|LSC:1&LGG:400||LSC:100|||30000000000|||||5||||||LS|LS|||1.3|C3.8
|
||||||
LGW|1|LGF:1&LSC:200||LGF:1000|||60000000000|||||10000||||||LS|LS|||44228|C4.1
|
LGW|1|LGF:1&LSC:200||LGF:1000|||60000000000|||||10000||||||LS|LS|||1.2|C4.1
|
||||||
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||44197|C4.4
|
LUB|1|LGW:1&LGF:2000||LGW:1000|||100000000000|||||1000000000||||||LS|LS|||1.1|C4.4
|
||||||
GG|1|GG:1||||||||||||||||Planet|BPlanet||||
|
GG|1|GG:1||||||||||||||||Planet|BPlanet||||
|
||||||
AB|1|AB:1||||||||||||||||Planet|BPlanet||||
|
AB|1|AB:1||||||||||||||||Planet|BPlanet||||
|
||||||
RP|1|RP:1||||||||||||||||Planet|BPlanet||||
|
RP|1|RP:1||||||||||||||||Planet|BPlanet||||
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="keep"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="keep"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
uid://cunl2sqmcj1ff
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
[gd_resource type="AudioBusLayout" format=3 uid="uid://b5g7p85k5do0m"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
bus/1/name = &"Music"
|
||||||
|
bus/1/solo = false
|
||||||
|
bus/1/mute = false
|
||||||
|
bus/1/bypass_fx = false
|
||||||
|
bus/1/volume_db = 0.0
|
||||||
|
bus/1/send = &"Master"
|
||||||
|
bus/2/name = &"FX"
|
||||||
|
bus/2/solo = false
|
||||||
|
bus/2/mute = false
|
||||||
|
bus/2/bypass_fx = false
|
||||||
|
bus/2/volume_db = 0.0
|
||||||
|
bus/2/send = &"Master"
|
||||||
+14
-8
@@ -1,14 +1,17 @@
|
|||||||
|
[runnable_presets]
|
||||||
|
|
||||||
|
"Windows Desktop"="Windows Desktop"
|
||||||
|
|
||||||
[preset.0]
|
[preset.0]
|
||||||
|
|
||||||
name="Windows Desktop"
|
name="Windows Desktop"
|
||||||
platform="Windows Desktop"
|
platform="Windows Desktop"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="../../Documents/Owad/Owniverse.I/Builds/owniverse.0.6.13-129.exe"
|
export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.console.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -27,7 +30,7 @@ script_export_mode=2
|
|||||||
custom_template/debug=""
|
custom_template/debug=""
|
||||||
custom_template/release=""
|
custom_template/release=""
|
||||||
debug/export_console_wrapper=1
|
debug/export_console_wrapper=1
|
||||||
binary_format/embed_pck=false
|
binary_format/embed_pck=true
|
||||||
texture_format/s3tc_bptc=true
|
texture_format/s3tc_bptc=true
|
||||||
texture_format/etc2_astc=false
|
texture_format/etc2_astc=false
|
||||||
shader_baker/enabled=false
|
shader_baker/enabled=false
|
||||||
@@ -74,7 +77,6 @@ Remove-Item -Recurse -Force '{temp_dir}'"
|
|||||||
|
|
||||||
name="Android"
|
name="Android"
|
||||||
platform="Android"
|
platform="Android"
|
||||||
runnable=false
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
@@ -110,7 +112,7 @@ architectures/armeabi-v7a=false
|
|||||||
architectures/arm64-v8a=true
|
architectures/arm64-v8a=true
|
||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=131
|
version/code=146
|
||||||
version/name=""
|
version/name=""
|
||||||
package/unique_name="com.KIDGameProduction.Owniverse"
|
package/unique_name="com.KIDGameProduction.Owniverse"
|
||||||
package/name="Owniverse"
|
package/name="Owniverse"
|
||||||
@@ -136,11 +138,12 @@ screen/support_normal=true
|
|||||||
screen/support_large=true
|
screen/support_large=true
|
||||||
screen/support_xlarge=true
|
screen/support_xlarge=true
|
||||||
screen/background_color=Color(0, 0, 0, 1)
|
screen/background_color=Color(0, 0, 0, 1)
|
||||||
|
splash_screen/disable_godot_boot_splash=false
|
||||||
|
splash_screen/icon=""
|
||||||
|
splash_screen/branding_image=""
|
||||||
|
splash_screen/background_color=Color(0, 0, 0, 1)
|
||||||
user_data_backup/allow=false
|
user_data_backup/allow=false
|
||||||
command_line/extra_args=""
|
command_line/extra_args=""
|
||||||
apk_expansion/enable=false
|
|
||||||
apk_expansion/SALT=""
|
|
||||||
apk_expansion/public_key=""
|
|
||||||
permissions/custom_permissions=PackedStringArray()
|
permissions/custom_permissions=PackedStringArray()
|
||||||
permissions/access_checkin_properties=false
|
permissions/access_checkin_properties=false
|
||||||
permissions/access_coarse_location=false
|
permissions/access_coarse_location=false
|
||||||
@@ -295,3 +298,6 @@ permissions/write_sms=false
|
|||||||
permissions/write_social_stream=false
|
permissions/write_social_stream=false
|
||||||
permissions/write_sync_settings=false
|
permissions/write_sync_settings=false
|
||||||
permissions/write_user_dictionary=false
|
permissions/write_user_dictionary=false
|
||||||
|
apk_expansion/enable=false
|
||||||
|
apk_expansion/SALT=""
|
||||||
|
apk_expansion/public_key=""
|
||||||
|
|||||||
Binary file not shown.
+5
-5
@@ -8,24 +8,24 @@
|
|||||||
|
|
||||||
config_version=5
|
config_version=5
|
||||||
|
|
||||||
[animation]
|
[accessibility]
|
||||||
|
|
||||||
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
general/updates_per_second=10
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Owniverse"
|
config/name="Owniverse"
|
||||||
config/version="0.6.15"
|
config/version="0.7.0"
|
||||||
run/main_scene="uid://bv384dpmvjv8o"
|
run/main_scene="uid://bv384dpmvjv8o"
|
||||||
config/features=PackedStringArray("4.7", "Mobile")
|
config/features=PackedStringArray("4.7", "Mobile")
|
||||||
boot_splash/show_image=false
|
boot_splash/show_image=false
|
||||||
config/icon="uid://do5teb1i7uiuw"
|
config/icon="uid://djiwkyk470qb2"
|
||||||
config/size/borderless=true
|
config/size/borderless=true
|
||||||
config/stretch_mode=0
|
config/stretch_mode=0
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
Global="*uid://dojk16crca3cx"
|
Global="*uid://dy0hrqie4ritn"
|
||||||
SaveManager="*uid://ul0srjc4gcgg"
|
SaveManager="*uid://ul0srjc4gcgg"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Эволюция звёзд Owniverse.GI
|
||||||
|
|
||||||
|
Числа симуляции хранят целую часть в двух блоках int64, дробный остаток — в
|
||||||
|
float. Весь порядок E27 сохраняет целые единицы. Возраст разделён на целые
|
||||||
|
годы и дробную часть с шагом 10^-9 года; большие промежуточные произведения
|
||||||
|
распределений вычисляются через `mul_div`. Границы точности, допуск переноса
|
||||||
|
дробей и несовместимый со старым формат сохранения 4 описаны в
|
||||||
|
[документации математики](world/owniverse/math/README.md).
|
||||||
|
|
||||||
|
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется.
|
||||||
|
|
||||||
|
## Порядок расчёта
|
||||||
|
|
||||||
|
1. Накопить игровое время; обработать новые десятые доли года.
|
||||||
|
2. Рассчитать производство существующих объектов за интервал с учётом их постепенного исчезновения; выполнить автоматическое создание звёзд и их планет.
|
||||||
|
3. Для очередного поколения определить долю заканчивающих жизнь звёзд. Применить локальное разрушение их планет до списания партий.
|
||||||
|
4. Снять целые объекты из партий, рассчитать основные и альтернативные выходы с накоплением дробей.
|
||||||
|
5. Передать выжившие планеты полученным звёздам/остаткам. Если преемников несколько, распределить по их фактическим количествам; если преемника нет — освободить. Остальные связанные объекты используют существующие правила GI.
|
||||||
|
6. Создать партии преемников. Учесть производство новорождённых за оставшуюся часть длинного шага, включая излучение короткоживущих сверхновых.
|
||||||
|
7. Применить суммарный бласт поколения; повторять обработку поколений, пока в интервале остаются переходы.
|
||||||
|
8. Пересчитать группы и опубликовать состояние.
|
||||||
|
|
||||||
|
## Автоматическое создание бурых карликов
|
||||||
|
|
||||||
|
На горячие бурые карлики `HBD` выделяется 0,1% **нового фактически произведённого автоматического водорода** за расчётный интервал. По текущей таблице автоматическую долю имеют STN (`auto = 0.5`) и ASTN (`auto = 0.65`). Обычный водород, ручные покупки и неиспользованный запас прошлых интервалов повторно в эту долю не входят.
|
||||||
|
|
||||||
|
При стоимости `c = 0.1 H` квота объектов увеличивается на `ΔQ = ΔH_auto × 0.001 / c`. Она хранится между циклами и в сохранении в `auto_star_deficits.HBD`. Доступность ограничивается автоматическим запасом и уже оплаченным топливом: `Q ≤ (auto_resources.H + HBD_fuel) / c`.
|
||||||
|
|
||||||
|
Карлики появляются по одному. Для `q = max(0, Q)` создаётся `floor(q)` объектов и ещё один с вероятностью `q - floor(q)`, с ограничением по доступному топливу. Созданное количество вычитается из Q; отрицательный остаток компенсирует досрочное появление на следующих шагах. Повторного случайного округления без нового автоматического водорода нет. Это исключение из общего детерминированного округления GI. Состояние генератора `auto_star_deficits.HBD_random` сохраняется, поэтому загрузка продолжает ту же последовательность.
|
||||||
|
|
||||||
|
Водород списывается целыми единицами авансом: `charge = ceil(max(0, count × c - HBD_fuel))`. Неиспользованный остаток `HBD_fuel += charge - count × c` сохраняется в `auto_star_deficits.HBD_fuel` и больше не входит в свободный запас или бюджет обычных звёзд. Например, 500 автоматических H позволяют создать 5 HBD, списав 1 H и оставив 0,5 H оплаченного топлива. Следующие 5 HBD используют этот остаток без повторного списания. Численность всегда целая, но не обязана быть кратной 10. При отсутствии обычных звёзд их доля остаётся в запасе, а производство HBD всё равно работает.
|
||||||
|
|
||||||
|
Новые HBD получают обычные жизненные партии. По таблице их срок — 5 миллиардов лет, затем они постепенно превращаются в холодные `CBD`. На экране оба типа суммируются в `BD`. Ручного рецепта у общего счётчика BD нет. Миграция сохранений не требуется: квота использует существующий словарь, отсутствующее значение равно нулю.
|
||||||
|
|
||||||
|
## Постепенные партии
|
||||||
|
|
||||||
|
### Короткоживущие звёздные события внутри длинного шага
|
||||||
|
|
||||||
|
Для SN, DSN, BSN, HN и GRB-событий полностью завершившиеся окна рождения не создают отдельные словари партий. Их целые количества суммируются в очередь завершений текущего расчёта; обычные партии сохраняются только для окон, срок которых ещё не закончился. Состояние этой очереди не записывается в сохранение: она полностью обрабатывается до публикации нового состояния.
|
||||||
|
|
||||||
|
Окно завершено, если `window_end + lifespan ≤ to_year`. Его доля количества рассчитывается прежним методом разности накопленных целых значений. Для производства пространства интеграл исчезновения вычитается по окнам в прежнем порядке, чтобы сохранить округление. Здесь остаётся лёгкий арифметический проход по окнам, но отсутствуют создание, копирование, сортировка и последующая обработка тысяч временных словарей.
|
||||||
|
|
||||||
|
Ресурсы, остатки звёзд и бласты от завершившейся части применяются на следующем поколении расчёта, как раньше. Поэтому сохраняются порядок передачи привязок, среда бластов и ограничения цивилизаций. Ускорение допускается только для событий группы `SNGRBB` с линейным выходом и без преемников с ненулевым сроком жизни. При изменении таблиц остальные случаи автоматически используют обычный путь. Сроки жизни и ширина окон не увеличиваются, формат сохранения не меняется.
|
||||||
|
|
||||||
|
На сохранении возраста 140,55 млн лет, при шестой скорости и шаге 1/60 секунды: максимальное временное число партий SN/GRB уменьшилось с 1668 до 2; средний расчёт кадра без отрисовки — примерно с 15 до 10 мс. За 120 шагов результаты старого и нового путей совпали по всем полям состояния. Эти значения характеризуют конкретное сохранение, а не гарантированную частоту кадров на всех устройствах.
|
||||||
|
|
||||||
|
### Общие окна рождения и эволюции
|
||||||
|
|
||||||
|
Окно рождения шириной `w = lifespan / 40` при `lifespan > 10`, иначе `w = lifespan`. Окно превращения сдвинуто на `lifespan`. При частичном списании:
|
||||||
|
|
||||||
|
```
|
||||||
|
expected = remainder + (remaining_count - remainder) * elapsed / remaining_window
|
||||||
|
converted = floor(expected)
|
||||||
|
remainder = expected - converted
|
||||||
|
```
|
||||||
|
|
||||||
|
Конец окна списывает остаток. Переход не уничтожает всю партию в начале окна. Начала и длительности сохраняют модель GI, включая годовую длительность событий SN/GRB.
|
||||||
|
|
||||||
|
## Планеты
|
||||||
|
|
||||||
|
При ручном и автоматическом создании звёзд:
|
||||||
|
|
||||||
|
```
|
||||||
|
expected = stars_created * planet_amount * planet_chance * metallicity + remainder
|
||||||
|
planets_created = floor(expected)
|
||||||
|
remainder = expected - planets_created
|
||||||
|
```
|
||||||
|
|
||||||
|
Остаток независим для каждой пары звезда/планета и сохраняется. Планеты сразу связаны со звездой. Эволюционный переход сам по себе не создаёт новую планетную систему. Шансы планет в таблице — доли, а проценты альтернативных выходов звёзд делятся на 100.
|
||||||
|
|
||||||
|
## Среда: адаптация формул Unity к состоянию GI
|
||||||
|
|
||||||
|
GI не хранит накопленные счётчики первичной материи Unity. Поэтому `stellar_environment()` вычисляет среду из текущего физического состава, включая связанные объекты, без суммирующих групп интерфейса. Это приближение для баланса GI, а не побитовое воспроизведение Unity.
|
||||||
|
|
||||||
|
- `M` — свободные и связанные H/He/Met плюс табличная масса конкретных объектов и масса SMBH.
|
||||||
|
- `V` — физическое S плюс табличный объём объектов и `voids * 1e22`.
|
||||||
|
- `Z = max(0, 0.4 * ln(100 * free_Met / M + 0.11) + 0.88)`; при `M = 0` металличность нулевая.
|
||||||
|
- `B = 65536 * f(M)`, где `f(0)=1`, `f(M)=M^0.1` при `0<M<1e10`, иначе `10`.
|
||||||
|
- `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`.
|
||||||
|
- `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`.
|
||||||
|
|
||||||
|
Планетные бонусы сил пока не перенесены. Параметры мира Bur/Exp/Agg имеют собственные сохраняемые шкалы и эффективные множители (см. раздел индикаторов ниже). Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
|
||||||
|
|
||||||
|
## Разрушение и бласты
|
||||||
|
|
||||||
|
Локальное воздействие: `ratio = dying_stars / physical_stars`, без множителя плотности.
|
||||||
|
|
||||||
|
Бласт: `hits = floor(power * blast_factor + saved_remainder)`. Распределение экспозиции между типами звёзд, имеющими планетные системы в таблице, пропорционально их физическому количеству. Для типа: `ratio = hits * star_share / physical_stars * density`.
|
||||||
|
|
||||||
|
Для каждого связанного типа планет накапливается `held_planets * min(1, ratio)`. Уничтожается целая часть, не больше существующего количества. Остатки локального воздействия и бластов раздельны. Это позволяет слабым воздействиям работать при постепенной эволюции.
|
||||||
|
|
||||||
|
Уровень 1 превращает только `blastability=1` в RP (HP, EON, цивилизации до C1.2). C1.3 и выше защищены от этого уровня. Уровень 2 уничтожает все планеты, включая защищённые цивилизации, без создания RP. Уничтоженные объекты удаляются из жизненных партий, чтобы не возникать заново.
|
||||||
|
|
||||||
|
Единственный источник мощности — поле `blast` сущности: `power = expired_count × max(0, entity.blast)`. При `blast = 0` внешнего воздействия нет. Старые вхождения `blast:N` в `output` игнорируются, включая альтернативные выходы; двойного суммирования нет. Внутренний выход `blast` остаётся событием, а не предметом. `destroy_level` независимо задаёт уровень локального разрушения у умирающего объекта; внешние бласты всегда действуют на уровне 1.
|
||||||
|
|
||||||
|
## Границы этапа
|
||||||
|
|
||||||
|
Рассчитываются агрегаты по типам, а не отдельные звёздные системы; конкретная принадлежность каждой планеты отдельной звезде не моделируется. Распределения целых объектов детерминированы. Порядок внутри длинного шага — по поколениям, поэтому нелинейное воздействие бластов и среды не обязано совпадать с серией более коротких шагов. Эволюция HP и эонов подключена ниже; эволюция цивилизаций и бонусы структур описаны ниже. Глобальная система сил остаётся отдельной механикой.
|
||||||
|
|
||||||
|
Проверки: `tests/test_time.gd`, `tests/test_stellar_rules.gd`. Сохранения игрока тестами не затрагиваются.
|
||||||
|
|
||||||
|
## Первичная жизнь: HP → EON1 → … → EON5 → C0.0
|
||||||
|
|
||||||
|
Правила хранятся в `data/life_rules.txt`, исходные сроки и коэффициенты взяты из Unity `civs.txt`. В каждой обработанной части партии:
|
||||||
|
|
||||||
|
- прогресс: `count * progress + carry_progress`;
|
||||||
|
- деградация: `count * degradation + carry_degradation`;
|
||||||
|
- остальная жизнь остаётся на прежней стадии и получает новую партию для следующей попытки.
|
||||||
|
|
||||||
|
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
|
||||||
|
|
||||||
|
Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже.
|
||||||
|
|
||||||
|
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
|
||||||
|
|
||||||
|
Переходы сохраняют количество планет и их хозяев, включая передачу преемнику звезды и разрушение бластом. Повторная попытка той же стадии не увеличивает статистику созданных объектов. Партии новых планет регистрируются при создании; недостающие партии старых HP/эонов — однократно при загрузке с текущим возрастом вселенной, без выдумывания их прошлого возраста. Состояние содержит `life_version=2` (версия 1 также загружается) и `life_remainders`.
|
||||||
|
|
||||||
|
Миграция версии жизни 1 → 2 добавляет только недостающие партии C0.0/C0.1, сохраняя уже существующие партии HP и эонов.
|
||||||
|
|
||||||
|
## Этические и космические цивилизации
|
||||||
|
|
||||||
|
`civilization_rules.txt` содержит 43 стадии по шесть этик из Unity. Они загружаются в `Global.CivilizationRules` и участвуют в расчёте партий. После C4.4 следует Ct: конечная стадия без новых жизненных партий.
|
||||||
|
|
||||||
|
Состояние `ethics[stage][ethic]` хранит целые количества. Для существующих цивилизаций без этик количество распределяется поровну в порядке m, l, w, d, c, b; остаток распределяется по одному в этом порядке. Существующее распределение сохраняется. При внешних потерях оно пропорционально сокращается.
|
||||||
|
|
||||||
|
Цивилизации с непустым `home` (C2.0–C4.4) освобождаются от прежних привязок без изменения общего количества. Общий механизм переходов больше не переносит планетную привязку на такого преемника. Структуры назначаются отдельно в `civilization_homes`; при возвращении к планетным стадиям цивилизации размещаются у имеющихся карликовых звёзд.
|
||||||
|
|
||||||
|
Правило среды согласовано: число космических цивилизаций ограничено звёздным ресурсом. Для стадии `i` расход ёмкости — `population[i] * max(1, Stars[i])`. Каждое семейство `Home` имеет общий бюджет для всех использующих его стадий; звёзды внутри структур также учитываются, но повторно не суммируются. SMBH учитывается по суммарной массе в эквиваленте звёзд, согласно решению пользователя.
|
||||||
|
|
||||||
|
Переход переиспользует резерв исходной стадии. При нехватке ёмкости неподдержанные переходы остаются на исходной стадии. Лимит применяется к общему механизму выходов партий и ручному созданию; расчёт прогресса C0.2+ использует тот же бюджет.
|
||||||
|
|
||||||
|
При загрузке и после эволюционных поколений избыток деградирует последовательно до поддерживаемой стадии. Приоритет получают существующие более развитые цивилизации; остаток бюджета достаётся младшим. Деградация переносит количество и этики, сокращает старые партии и регистрирует новые, не уничтожая цивилизации. C2.0 при избытке становится C1.9 и распределяется по имеющимся звёздам-карликам. Если карликов нет, C1.9 остаётся свободной — отсутствующая звезда не создаётся. Планетные стадии до C2.0 не расходуют отдельную звёздную ёмкость: у одной звезды могут быть несколько планет.
|
||||||
|
|
||||||
|
Резерв ёмкости логический: звёзды не расходуются и продолжают производить ресурсы. Общий бюджет пересчитывается из физических количеств, отдельная копия бронирований в сохранении не хранится.
|
||||||
|
|
||||||
|
|
||||||
|
### Расчёт поколения C0.2–C4.4
|
||||||
|
|
||||||
|
1. Из истёкшей части партии пропорционально выбираются представители шести этик. Для каждой этики читаются `progress` и `degradation`; целые исходы получаются с сохранением дробных остатков. Неудавшийся из-за лимита переход возвращается на исходную стадию.
|
||||||
|
2. Прогресс ведёт на следующую стадию; деградация распределяется по весам `fall` конкретной этики. Повторные попытки не увеличивают статистику созданных цивилизаций.
|
||||||
|
3. При флаге `fights` рассчитываются войны. Веса исходов — 1:2:3:3, доли прогресса победителей — 0:0.25:0.5:0.75; первый исход деградирует атакующих. Потери противников — 2:1:1.5:2 на войну. Два последних исхода дают 10 и 30 единиц побочного ущерба предыдущей стадии. Выбор участников ограничен живой популяцией, включая ещё не сработавшие партии; их партии сокращаются вместе с населением.
|
||||||
|
4. Для обычного дрейфа веса своей/противоположной/каждой другой этики равны 196/0/1. Дипломатия умножает исходные численности на `diplomacy_rate`, затем нормирует к прежней численности. Флаг дипломатии действует на остающихся на стадии. При переходе в Ct этика сохраняется без дрейфа.
|
||||||
|
5. При флаге `colonizes` создаются колонии на две стадии ниже отправителя. Цели — существующие GG, AB, RP, HP; колонизация заменяет цель и сохраняет её звёздную привязку. Веса дрейфа колонистов — 960/4/9. Исходная цивилизация не расходуется. Новая партия начинается в текущий игровой момент.
|
||||||
|
6. После применения переходов пересчитываются этики, поклонение (`Σ population × worship`), бласты и ограничения среды. Число операций зависит от числа типов и партий, а не от числа цивилизаций.
|
||||||
|
|
||||||
|
Окна исходных партий сохраняют время переходов; несколько частей одного типа обрабатываются агрегированно между минимальным и максимальным временем. Побочный ущерб неистёкшим партиям применяется в текущий момент. Все остатки округления сохраняются в `civilization_remainders`, счётчики — в `civilization_stats`.
|
||||||
|
|
||||||
|
### Структуры и периоды
|
||||||
|
|
||||||
|
Прямой `claim` задаёт структуру стадии и норму 1; каждая промежуточная стадия умножает норму на 10 до следующего `claim`. Начиная с C3.0 требуется соответствующий запас структур, общий для использующих его стадий. При потере запаса избыток деградирует. Этот бюджет действует одновременно со звёздным.
|
||||||
|
|
||||||
|
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса.
|
||||||
|
|
||||||
|
Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит.
|
||||||
|
|
||||||
|
## Параметры мира, тенденции и индикаторы
|
||||||
|
|
||||||
|
Источник формул — Unity `OwniverseInstance`, `Life`, `LifeActions`, `Trends` и классы индикаторов. Расчёты находятся в `world_parameters.gd`; показ — в `ui/indicators/world_indicators.gd`. Шкалы обновляются каждые 0,1 реальной секунды и сразу при создании/загрузке вселенной. Подписи используют текущую локаль и шаблоны.
|
||||||
|
|
||||||
|
### Энергия и плотность
|
||||||
|
|
||||||
|
Энергия сохраняет экономику GI: запас расходуется на H/S. Заполнение индикатора — `clamp(energy / 1e14, 0, 1)`. Это не коэффициент производства Unity; дополнительное замедление производства при уменьшении запаса не вводится. В описании выводятся остаток и процент.
|
||||||
|
|
||||||
|
Плотность берётся из `stellar_environment`. Положение указателя ограничено диапазоном 0…1:
|
||||||
|
|
||||||
|
- `density ≤ 1e-5`: шкала разрыва, `(log10(max(density, 1e-7)) + 7) / 2`;
|
||||||
|
- `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`;
|
||||||
|
- `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`.
|
||||||
|
|
||||||
|
В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. Само наступление апокалипсиса индикатор не запускает.
|
||||||
|
|
||||||
|
Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами, не перехватывает нажатия и снимается при восстановлении плотности. Необратимая последовательность апокалипсиса Unity сюда не входит.
|
||||||
|
|
||||||
|
### Горючесть, взрывчатость и агрессия
|
||||||
|
|
||||||
|
`world_parameters` хранит Bur, Exp и Agg в диапазоне 1…19, начальное значение — 10. Эффективный множитель `M(x) = 10^((x−10)/9)`: крайние значения 0,1 и 10, нейтральное — 1.
|
||||||
|
|
||||||
|
- Bur делит срок жизни новых звёзд на `M(Bur)`. `modify_world_parameter("Bur", delta)` также пересчитывает оставшееся время существующих звёзд с коэффициентом `старый M / новый M`, сохраняя количество и остатки округления. Производство звёзд умножается на `M`, если `M ≤ 1`, иначе на `sqrt(M)`.
|
||||||
|
- Exp умножает вероятность альтернативного исхода (с ограничением до 1), мощность `entity.blast` и производство событий группы SNGRBB. Локальный `destroy_level` от него не меняется.
|
||||||
|
- Для Agg обозначим `a = M(Agg)`. Вероятность прогресса умножается на `a`, деградации — на `(1 − (1−a)/20) / Order`, войны — на `1 − (1−a)/5`, колонизации — на `1 − (1−a)/2`. Если сумма прогресса и деградации превышает 1, они пропорционально нормируются. Вероятности войны/колонизации ограничены единицей.
|
||||||
|
- Для первичной жизни дополнительный множитель прогресса — Enlightenment. Order дополнительно уменьшает потери в войнах и побочный ущерб. Начальные Enlightenment и Order равны 1.
|
||||||
|
|
||||||
|
Изменять шкалы следует через `owniverse.modify_world_parameter(key, delta)`, чтобы горючесть корректно обновляла партии. Готовых кнопок применения сил этот метод не добавляет.
|
||||||
|
|
||||||
|
### Тенденции
|
||||||
|
|
||||||
|
Пять направлений: Antitheism, Integration, Xenocide, Reconstruction, Spirituality. Участвуют космические стадии с непустым `home`; вес первой — 1, каждой следующей — вдвое больше. Для каждой тенденции:
|
||||||
|
|
||||||
|
`rate = Σ_стадий (вес стадии × Σ_этик(доля этики × вес этики))`
|
||||||
|
|
||||||
|
`прирост = rate × прошедшие игровые годы × 1e-9`.
|
||||||
|
|
||||||
|
Доли берутся на начало интервала, после проверки вместимости. Умножение на время исправляет ошибку Unity, где аргумент period не использовался. Накопление само по себе не зависит от FPS; изменение состава цивилизаций внутри крупных шагов остаётся дискретным, как остальная симуляция.
|
||||||
|
|
||||||
|
Веса этик в порядке пяти направлений:
|
||||||
|
|
||||||
|
| Этика | Антитеизм | Интеграция | Ксеноцид | Реконструкция | Духовность |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| m | 0 | 2 | 2 | 0 | 6 |
|
||||||
|
| l | 6 | 1 | 1 | 2 | 0 |
|
||||||
|
| w | 2 | 0 | 6 | 0 | 2 |
|
||||||
|
| d | 0 | 6 | 0 | 1 | 3 |
|
||||||
|
| c | 1 | 3 | 3 | 3 | 0 |
|
||||||
|
| b | 3 | 0 | 0 | 6 | 1 |
|
||||||
|
|
||||||
|
При достижении 1 учитываются все целые срабатывания, дробный остаток сохраняется. Только Integration имеет последствия в исходной Unity: равновероятно выбирается Federation (+0,1 и Agg −1), Enlightenment (+0,2 и Agg −1) или Order (+0,1). Генератор сохраняет своё состояние. До 256 событий за шаг выбор выполняется по одному; для больших количеств используется нормальная аппроксимация мультиномиального распределения с сохранением точного суммарного числа событий, чтобы высокие скорости не блокировали кадр. Она может менять последовательность исходов при другом разбиении больших интервалов.
|
||||||
|
|
||||||
|
Federation сохраняется и увеличивается как в Unity, но в исходном коде не применяется к дипломатии. Дополнительное правило для него не придумано. Остальные четыре тенденции имеют счётчики срабатываний и сброс, но не последствия — так же, как в исходной реализации. Параметры и остатки тенденций сохраняются; у существующего сохранения текущего формата без этих полей используются нейтральные значения и нулевые тенденции, без изменения ресурсов или возраста.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+325
-97
@@ -1,59 +1,78 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
||||||
|
|
||||||
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
|
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
|
||||||
|
|
||||||
|
const ItemDescription = preload("res://world/owniverse/item_description.gd")
|
||||||
|
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
|
||||||
|
var world_indicators: RefCounted
|
||||||
|
var indicator_elapsed: float = 0.0
|
||||||
|
var description_targets: Dictionary = {}
|
||||||
|
var description_samples: Dictionary = {}
|
||||||
|
var description_rates: Dictionary = {}
|
||||||
|
var description_elapsed: float = 0.0
|
||||||
|
|
||||||
const PAGE_COUNT = 5
|
const PAGE_COUNT = 5
|
||||||
|
const RESOURCE_PAGE = 1
|
||||||
const PAGE_WIDTH = 1080
|
const PAGE_WIDTH = 1080
|
||||||
const DRAG_TOLERANCE = 360
|
const DRAG_TOLERANCE = 360
|
||||||
const SWIPE_DURATION: float = 0.4
|
const SWIPE_DURATION: float = 0.4
|
||||||
|
|
||||||
@onready var stage_page_scroller: ScrollContainer = $StageRows/PageScroller
|
@onready var stage_page_scroller: ScrollContainer = $World/PageScroller
|
||||||
|
|
||||||
@onready var item_pages: Array[Control] = [
|
@onready var item_pages: Array[Control] = [
|
||||||
$StageRows/PageScroller/HBoxContainer/ForcePage/Forces,
|
$World/PageScroller/HBoxContainer/ForcePage/Forces,
|
||||||
$StageRows/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources,
|
$World/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources,
|
||||||
$StageRows/PageScroller/HBoxContainer/StarPage/Stars,
|
$World/PageScroller/HBoxContainer/StarPage/Stars,
|
||||||
$StageRows/PageScroller/HBoxContainer/StructurePage/Structures,
|
$World/PageScroller/HBoxContainer/StructurePage/Structures,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Life,
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Life,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet,
|
$World/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ0,
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ0,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ1,
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ1,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ2,
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ2,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ3,
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ3,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ4
|
$World/PageScroller/HBoxContainer/LifePage/Planets/Civ4
|
||||||
]
|
]
|
||||||
|
|
||||||
@onready var page_actions: Dictionary[int, OwniverseAction] = {
|
@onready var page_actions: Dictionary[int, OwniverseAction] = {
|
||||||
2: $StageRows/PageScroller/HBoxContainer/StarPage/Actions,
|
2: $World/PageScroller/HBoxContainer/StarPage/Actions,
|
||||||
3: $StageRows/PageScroller/HBoxContainer/StructurePage/Actions,
|
3: $World/PageScroller/HBoxContainer/StructurePage/Actions,
|
||||||
}
|
}
|
||||||
|
|
||||||
@onready var g_item_descriptions: Array = [
|
@onready var g_item_descriptions: Array = [
|
||||||
$StageRows/PageScroller/HBoxContainer/ForcePage/Description,
|
$World/PageScroller/HBoxContainer/ForcePage/Description,
|
||||||
$StageRows/PageScroller/HBoxContainer/ResourcePage/Description,
|
$World/PageScroller/HBoxContainer/ResourcePage/Description,
|
||||||
$StageRows/PageScroller/HBoxContainer/StarPage/Description,
|
$World/PageScroller/HBoxContainer/StarPage/Description,
|
||||||
$StageRows/PageScroller/HBoxContainer/StructurePage/Description,
|
$World/PageScroller/HBoxContainer/StructurePage/Description,
|
||||||
$StageRows/PageScroller/HBoxContainer/LifePage/Description
|
$World/PageScroller/HBoxContainer/LifePage/Description
|
||||||
]
|
]
|
||||||
|
|
||||||
@onready var date_time: Label = $StageRows/TopMenu/InfoPanel/DateTime
|
@onready var date_time: Label = $World/TopMenu/InfoPanel/DateTime
|
||||||
|
|
||||||
@onready var g_speed_icons: Array = [
|
@onready var g_speed_icons: Array = [
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed1,
|
$World/TopMenu/InfoPanel/Speed/Speed1,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed2,
|
$World/TopMenu/InfoPanel/Speed/Speed2,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed3,
|
$World/TopMenu/InfoPanel/Speed/Speed3,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed4,
|
$World/TopMenu/InfoPanel/Speed/Speed4,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed5,
|
$World/TopMenu/InfoPanel/Speed/Speed5,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed6,
|
$World/TopMenu/InfoPanel/Speed/Speed6,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed7,
|
$World/TopMenu/InfoPanel/Speed/Speed7,
|
||||||
$StageRows/TopMenu/InfoPanel/Speed/Speed8
|
$World/TopMenu/InfoPanel/Speed/Speed8
|
||||||
]
|
]
|
||||||
|
|
||||||
@onready var g_time_minus: TextureButton = $StageRows/TopMenu/TimeMinus
|
@onready var g_time_minus: TextureButton = $World/TopMenu/TimeMinus
|
||||||
@onready var g_time_plus: TextureButton = $StageRows/TopMenu/TimePlus
|
@onready var g_time_plus: TextureButton = $World/TopMenu/TimePlus
|
||||||
|
|
||||||
|
@onready var screens: Dictionary = {
|
||||||
|
"World": $World,
|
||||||
|
"MainMenu": $MainMenu,
|
||||||
|
"Report": $Report,
|
||||||
|
"Achievements": $Achievements,
|
||||||
|
"Credits": $Credits,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var g_tween: Tween = null
|
var g_tween: Tween = null
|
||||||
var g_dragged_start: Vector2 = Vector2.ZERO
|
var g_dragged_start: Vector2 = Vector2.ZERO
|
||||||
@@ -71,52 +90,80 @@ var g_counters: Dictionary[String, Counter] = {
|
|||||||
|
|
||||||
var g_active_items: Dictionary = {}
|
var g_active_items: Dictionary = {}
|
||||||
|
|
||||||
|
var pending_page: int = -1
|
||||||
|
var active_actions_dirty: bool = true
|
||||||
|
var active_actions_elapsed: float = 0.0
|
||||||
|
|
||||||
var owniverse:Owniverse = Owniverse.new()
|
var owniverse:Owniverse = Owniverse.new()
|
||||||
var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
||||||
|
var locked_items: Array = []
|
||||||
|
|
||||||
var item_list_to_update: Dictionary = {}
|
|
||||||
|
|
||||||
func set_datetime(value: float):
|
func set_datetime(value):
|
||||||
date_time.text = str(snapped(value, 0.1))
|
# Simulation time advances in tenths of a year; displaying five decimal
|
||||||
|
# places only adds zeroes and suggests precision the simulation does not
|
||||||
|
# have.
|
||||||
|
date_time.text = Global.format_number(value, 1) if not E.eq(value, 0) else "0.0"
|
||||||
|
|
||||||
#region Item manipulations: update and visibility
|
#region Item manipulations: update and visibility
|
||||||
|
|
||||||
|
|
||||||
func add_to_update_list(item_id: String, amount: int) -> void:
|
|
||||||
if item_list_to_update.has(item_id):
|
|
||||||
item_list_to_update[item_id] += amount
|
|
||||||
else:
|
|
||||||
item_list_to_update[item_id] = amount
|
|
||||||
|
|
||||||
|
|
||||||
func set_owniverse_item_value() -> void:
|
func set_owniverse_item_value() -> void:
|
||||||
for item_id in owniverse.items_to_update:
|
for item_id in owniverse.presentation.pending_amounts:
|
||||||
if owniverse_items.has(item_id):
|
if owniverse_items.has(item_id):
|
||||||
owniverse_items[item_id].set_label(owniverse.items_to_update[item_id])
|
owniverse_items[item_id].set_label(owniverse.presentation.pending_amounts[item_id])
|
||||||
owniverse.items_to_update = {}
|
owniverse.presentation.pending_amounts.clear()
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _unlock_items():
|
||||||
|
var result: Dictionary = owniverse.presentation.unlocks(owniverse.state, owniverse.catalog, locked_items)
|
||||||
|
for item_id in result.visible:
|
||||||
|
if owniverse_items.has(item_id):
|
||||||
|
owniverse_items[item_id].set_visible(true)
|
||||||
|
locked_items = result.remaining
|
||||||
|
|
||||||
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
||||||
_set_description(indicator_name, page)
|
_set_description(indicator_name, page)
|
||||||
|
|
||||||
|
|
||||||
func _set_item_description(item: OwniverseItem) -> void:
|
func _set_item_description(item: OwniverseItem) -> void:
|
||||||
var description: String = "[b]" \
|
var category: String = "Entity" if owniverse.catalog.entities.has(item.item_id) else "Force"
|
||||||
+ Global.get_localized_item_description(item.item_name, "label") \
|
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
|
||||||
+"[/b]" \
|
var page: int = clampi(item.item_page, 0, 4)
|
||||||
+"\n" \
|
if description_targets.get(page, {}).get("id", "") != id:
|
||||||
+ Global.get_localized_item_description(item.item_name, "description")
|
description_samples.erase(id)
|
||||||
var desc_page = clamp(item.item_page, 0, 4)
|
description_rates.erase(id)
|
||||||
g_item_descriptions[desc_page].text = description
|
description_targets[page] = {"id": id, "category": category}
|
||||||
|
_refresh_descriptions()
|
||||||
|
|
||||||
|
|
||||||
func _set_description(item_name: String, page: int) -> void:
|
func _set_description(item_name: String, page: int) -> void:
|
||||||
var description = Global.get_description(item_name + ".Name")
|
description_targets[page] = {"id": item_name, "category": "Indicator"}
|
||||||
description += "\n"
|
_refresh_descriptions()
|
||||||
description += Global.get_description(item_name + ".Description")
|
|
||||||
g_item_descriptions[page].text = description
|
|
||||||
|
func _refresh_descriptions() -> void:
|
||||||
|
for page in description_targets:
|
||||||
|
var target: Dictionary = description_targets[page]
|
||||||
|
var label: RichTextLabel = g_item_descriptions[page]
|
||||||
|
label.bbcode_enabled = true
|
||||||
|
label.text = ItemDescription.render(owniverse, Global, target.id, target.category, description_rates.get(target.id, 0.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _update_descriptions(delta: float) -> void:
|
||||||
|
description_elapsed += delta
|
||||||
|
if description_elapsed < 0.25:
|
||||||
|
return
|
||||||
|
for page in description_targets:
|
||||||
|
var id: String = description_targets[page].id
|
||||||
|
var total = ItemDescription.quantities(owniverse, id).total
|
||||||
|
description_rates[id] = E.divide(E.subtract(total, description_samples[id]), E.of(description_elapsed)) if description_samples.has(id) else 0.0
|
||||||
|
description_samples[id] = total
|
||||||
|
description_elapsed = 0.0
|
||||||
|
_refresh_descriptions()
|
||||||
|
|
||||||
|
|
||||||
func _control_page(item: OwniverseItem) -> void:
|
func _control_page(item: OwniverseItem) -> void:
|
||||||
@@ -129,6 +176,7 @@ func _control_page(item: OwniverseItem) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
g_active_items[item.item_page] = item
|
g_active_items[item.item_page] = item
|
||||||
|
active_actions_dirty = true
|
||||||
|
|
||||||
if current_item != null and current_item.item_page == item.item_page:
|
if current_item != null and current_item.item_page == item.item_page:
|
||||||
current_item.set_selection(false)
|
current_item.set_selection(false)
|
||||||
@@ -169,10 +217,9 @@ func item_activated(item: OwniverseItem) -> void:
|
|||||||
_set_item_description(item)
|
_set_item_description(item)
|
||||||
|
|
||||||
if item.item_id == "S" or item.item_id == "H":
|
if item.item_id == "S" or item.item_id == "H":
|
||||||
owniverse.produce(item.item_id, 100_000)
|
var _value = owniverse.state.amount[item.item_id]
|
||||||
|
_value = E.multiply(9, _value) if E.gt(_value, 0) else 1
|
||||||
func _unlock_item(item_id: String):
|
owniverse.request_production(item.item_id, _value, true)
|
||||||
owniverse_items[item_id].set_visible(true)
|
|
||||||
|
|
||||||
|
|
||||||
func _check_counters(delta: float) -> Array:
|
func _check_counters(delta: float) -> Array:
|
||||||
@@ -191,7 +238,7 @@ func _check_counters(delta: float) -> Array:
|
|||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
#region Swipe
|
#region Swipe and page
|
||||||
|
|
||||||
func _do_actions(actions: Array) -> void:
|
func _do_actions(actions: Array) -> void:
|
||||||
for action: String in actions:
|
for action: String in actions:
|
||||||
@@ -226,15 +273,48 @@ func _input(event: InputEvent) -> void:
|
|||||||
g_counters.swipe.value = 0
|
g_counters.swipe.value = 0
|
||||||
elif event is InputEventScreenTouch:
|
elif event is InputEventScreenTouch:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _set_active_page_actions() -> void:
|
||||||
|
for pa in page_actions:
|
||||||
|
if !g_active_items.has(pa): continue
|
||||||
|
|
||||||
|
var item_id: String = g_active_items[pa].item_id
|
||||||
|
var entity: OwniverseEntity = owniverse.catalog.entities[item_id]
|
||||||
|
|
||||||
|
#var amount = owniverse.get_available_to_produce(item_id)
|
||||||
|
|
||||||
|
#var amount = owniverse.get_available_to_produce(entity)
|
||||||
|
var amount = owniverse.production.calculate_max_producible(entity)
|
||||||
|
#print(item_id, " : ", amount)
|
||||||
|
if item_id == "SMBH":
|
||||||
|
var preview: Callable = func(requested):
|
||||||
|
return owniverse.production.calculate_max_producible(entity, requested)
|
||||||
|
page_actions[pa].set_actions(item_id, amount, preview)
|
||||||
|
else:
|
||||||
|
page_actions[pa].set_actions(item_id, amount)
|
||||||
|
|
||||||
|
|
||||||
|
func _page_action_pressed(page_id: int, action_amount) -> void:
|
||||||
|
if !g_active_items.has(page_id):
|
||||||
|
return
|
||||||
|
|
||||||
|
var item_id: String = g_active_items[page_id].item_id
|
||||||
|
|
||||||
|
if E.gt(action_amount, 0):
|
||||||
|
owniverse.request_production(item_id, action_amount)
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
owniverse.init_enitities()
|
get_tree().auto_accept_quit = false
|
||||||
owniverse.load_data()
|
world_indicators = WorldIndicators.new(self)
|
||||||
_init_owniverse_items()
|
create_new_universe()
|
||||||
|
load_data()
|
||||||
|
_refresh_universe_ui()
|
||||||
|
|
||||||
owniverse.unlock_item.connect(_unlock_item)
|
|
||||||
g_counters.swipe.sec = 0.2
|
g_counters.swipe.sec = 0.2
|
||||||
|
|
||||||
for pa in page_actions:
|
for pa in page_actions:
|
||||||
@@ -242,39 +322,83 @@ func _ready() -> void:
|
|||||||
page_actions[pa].page_id = pa
|
page_actions[pa].page_id = pa
|
||||||
|
|
||||||
|
|
||||||
|
## Starts a fresh universe. Can be connected directly to a button's pressed signal.
|
||||||
|
## Saving continues through the normal autosave/close path.
|
||||||
|
func create_new_universe() -> void:
|
||||||
|
var previous: Owniverse = owniverse
|
||||||
|
owniverse = Owniverse.new()
|
||||||
|
var inited_result: Dictionary = init_enitities()
|
||||||
|
locked_items = inited_result.locked_items
|
||||||
|
|
||||||
|
for entity in previous.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
previous.free()
|
||||||
|
|
||||||
|
if is_instance_valid(g_tween):
|
||||||
|
g_tween.kill()
|
||||||
|
g_active_items.clear()
|
||||||
|
description_targets.clear()
|
||||||
|
description_samples.clear()
|
||||||
|
description_rates.clear()
|
||||||
|
description_elapsed = 0.0
|
||||||
|
active_actions_dirty = true
|
||||||
|
active_actions_elapsed = 0.0
|
||||||
|
for label in g_item_descriptions:
|
||||||
|
label.text = ""
|
||||||
|
for item in owniverse_items.values():
|
||||||
|
item.get_parent().remove_child(item)
|
||||||
|
item.queue_free()
|
||||||
|
_init_owniverse_items()
|
||||||
|
for page in range(5, item_pages.size()):
|
||||||
|
item_pages[page].visible = false
|
||||||
|
for actions in page_actions.values():
|
||||||
|
actions.set_actions("", 0.0)
|
||||||
|
pending_page = RESOURCE_PAGE
|
||||||
|
_restore_page.call_deferred()
|
||||||
|
_refresh_universe_ui()
|
||||||
|
_on_topmenu_button_pressed("World")
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_universe_ui() -> void:
|
||||||
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
|
_change_speed(0)
|
||||||
|
set_datetime(owniverse.state.time.year)
|
||||||
|
set_owniverse_item_value()
|
||||||
|
_unlock_items()
|
||||||
|
if world_indicators != null: world_indicators.update(owniverse)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
|
||||||
var year_delta: float = owniverse.add_time_delta(delta)
|
owniverse.evolute_frame(delta, get_tree())
|
||||||
set_datetime(year_delta)
|
indicator_elapsed += delta
|
||||||
owniverse.cycle(year_delta)
|
if indicator_elapsed >= 0.1:
|
||||||
|
indicator_elapsed = 0.0
|
||||||
|
world_indicators.update(owniverse)
|
||||||
|
_update_descriptions(delta)
|
||||||
|
set_owniverse_item_value()
|
||||||
|
set_datetime(owniverse.state.time.year)
|
||||||
|
|
||||||
|
#owniverse.cycle(delta)
|
||||||
|
|
||||||
|
#owniverse.cycle_legacy(year_delta)
|
||||||
|
|
||||||
var actions = _check_counters(delta)
|
var actions = _check_counters(delta)
|
||||||
_do_actions(actions)
|
_do_actions(actions)
|
||||||
|
# Production limits are expensive (they walk the resource ledger). Refresh
|
||||||
_set_active_page_actions()
|
# them when selection changes and at a modest cadence instead of every
|
||||||
|
# rendered frame. The simulation itself continues to run every frame.
|
||||||
func _set_active_page_actions() -> void:
|
active_actions_elapsed += delta
|
||||||
for pa in page_actions:
|
if active_actions_dirty or active_actions_elapsed >= 0.1:
|
||||||
if !g_active_items.has(pa): continue
|
active_actions_elapsed = 0.0
|
||||||
|
active_actions_dirty = false
|
||||||
var item_id: String = g_active_items[pa].item_id
|
_set_active_page_actions()
|
||||||
var amount = owniverse.get_available_to_produce(item_id)
|
|
||||||
page_actions[pa].set_actions(item_id, amount)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
|
||||||
if !g_active_items.has(page_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
var item_id: String = g_active_items[page_id].item_id
|
|
||||||
|
|
||||||
if action_amount > 0:
|
|
||||||
owniverse.produce(item_id, action_amount)
|
|
||||||
|
|
||||||
|
|
||||||
func _change_speed(delta: int) -> void:
|
func _change_speed(delta: int) -> void:
|
||||||
var speed: int = owniverse.change_speed(delta)
|
|
||||||
|
owniverse.speed = owniverse.speed + delta
|
||||||
|
|
||||||
|
var speed: int = owniverse.speed
|
||||||
var ind: int = 1
|
var ind: int = 1
|
||||||
for icon in g_speed_icons:
|
for icon in g_speed_icons:
|
||||||
if ind <= speed:
|
if ind <= speed:
|
||||||
@@ -293,14 +417,118 @@ func _change_speed(delta: int) -> void:
|
|||||||
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
||||||
|
|
||||||
|
|
||||||
func _on_topmenu_button_pressed(ButtonName) -> void:
|
func _on_topmenu_button_pressed(screen_name: String) -> void:
|
||||||
print(ButtonName)
|
|
||||||
#var s: float = 1E15
|
for screen_id in screens:
|
||||||
#var z = 1.0
|
#print(screen_name, " ←→ ", screen_id)
|
||||||
#for i in range(30):
|
screens[screen_id].visible = screen_id == screen_name
|
||||||
#print(i, " | ", z, " | ", " | ", s - z)
|
|
||||||
#z *= 10
|
|
||||||
|
|
||||||
|
|
||||||
func _on_save_data() -> void:
|
func _on_unlock_and_save_timeout() -> void:
|
||||||
owniverse.save_data()
|
save_data()
|
||||||
|
_unlock_items()
|
||||||
|
|
||||||
|
|
||||||
|
#region Init save load routines
|
||||||
|
|
||||||
|
|
||||||
|
func load_data() -> bool:
|
||||||
|
var game_data: Dictionary = SaveManager.load_data("owniverse", "qwerty")
|
||||||
|
if game_data.is_empty():
|
||||||
|
if SaveManager.last_error == "":
|
||||||
|
pending_page = RESOURCE_PAGE
|
||||||
|
_restore_page.call_deferred()
|
||||||
|
return false
|
||||||
|
var restored: Dictionary = game_data.state
|
||||||
|
# Current definitions are initialized first; reject incompatible saves before publishing.
|
||||||
|
for id in owniverse.state.amount:
|
||||||
|
if not restored.amount.has(id) or not restored.stats.total_amount.has(id):
|
||||||
|
SaveManager.blocked_saves["owniverse"] = true
|
||||||
|
push_error("Save is missing the current entity: " + id)
|
||||||
|
return false
|
||||||
|
var complete: Dictionary = owniverse.state.duplicate(true)
|
||||||
|
complete.merge(restored, true)
|
||||||
|
owniverse.state = complete
|
||||||
|
owniverse.restore_frame_checkpoint(game_data.get("simulation", {}))
|
||||||
|
|
||||||
|
owniverse.evolution.initialize_life_batches()
|
||||||
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
|
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
|
||||||
|
_restore_page.call_deferred()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _restore_page() -> void:
|
||||||
|
# Wait until containers have calculated the scrollable width.
|
||||||
|
await get_tree().process_frame
|
||||||
|
if pending_page >= 0:
|
||||||
|
stage_page_scroller.scroll_horizontal = pending_page * PAGE_WIDTH
|
||||||
|
pending_page = -1
|
||||||
|
|
||||||
|
|
||||||
|
func save_data() -> void:
|
||||||
|
var page: int = pending_page
|
||||||
|
if page < 0:
|
||||||
|
page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1)
|
||||||
|
SaveManager.request_save("owniverse", "qwerty", {
|
||||||
|
"state": owniverse.state,
|
||||||
|
"simulation": owniverse.frame_checkpoint(),
|
||||||
|
"ui": {"current_page": page},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
var closing: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if not is_node_ready() or closing:
|
||||||
|
return
|
||||||
|
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||||
|
_close_after_save()
|
||||||
|
elif what == NOTIFICATION_APPLICATION_PAUSED:
|
||||||
|
save_data()
|
||||||
|
# The OS may suspend frames immediately; finish the queued write now.
|
||||||
|
SaveManager.flush_saves()
|
||||||
|
|
||||||
|
|
||||||
|
func _close_after_save() -> void:
|
||||||
|
closing = true
|
||||||
|
process_mode = Node.PROCESS_MODE_DISABLED
|
||||||
|
save_data()
|
||||||
|
if SaveManager.is_saving():
|
||||||
|
await SaveManager.saves_drained
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
func init_enitities() -> Dictionary:
|
||||||
|
var _result := {
|
||||||
|
'locked_items': [],
|
||||||
|
}
|
||||||
|
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
||||||
|
for entity_id in owniverse.catalog.entities:
|
||||||
|
var entity: OwniverseEntity = owniverse.catalog.entities[entity_id]
|
||||||
|
|
||||||
|
owniverse.state.stats.total_amount[entity_id] = 0
|
||||||
|
owniverse.state.amount[entity_id] = 0
|
||||||
|
if E.gt(entity.lifespan, 0):
|
||||||
|
owniverse.state.batches[entity.id] = []
|
||||||
|
owniverse.batches.add_entity_to_batch(entity_id, 0, 0)
|
||||||
|
if entity.group_as != "":
|
||||||
|
owniverse.state.amount[entity.group_as] = 0
|
||||||
|
|
||||||
|
owniverse.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
|
||||||
|
if entity.unlock != {}:
|
||||||
|
_result.locked_items.append(entity.id)
|
||||||
|
|
||||||
|
return _result
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
func _on_achievements_pressed(extra_arg_0: String) -> void:
|
||||||
|
pass # Replace with function body.
|
||||||
|
|
||||||
|
|
||||||
|
func _on_credits_pressed(extra_arg_0: String) -> void:
|
||||||
|
pass # Replace with function body.
|
||||||
|
|||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
# Проверки поэтапного переноса из Owniverse.U
|
||||||
|
|
||||||
|
## Производительность на сохранении
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Читает диагностический JSON формата 4 в отдельные экземпляры мира и замеряет
|
||||||
|
шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается.
|
||||||
|
Последним аргументом можно задать длительность шага в реальных секундах
|
||||||
|
(по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.
|
||||||
|
|
||||||
|
На диагностической копии с 120 партиями прежняя рациональная арифметика после
|
||||||
|
оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6.
|
||||||
|
После перехода на целые блоки и устранения повторного построения физических
|
||||||
|
количеств повторный замер дал 53–58 мс. При шаге 0,1 секунды: 65–67 мс вместо
|
||||||
|
117–138 мс. Это замеры расчёта на данной
|
||||||
|
копии, а не обещание FPS. Для сравнения старый JSON был преобразован только
|
||||||
|
во временный тестовый файл; в загрузчик игры миграция не добавлена.
|
||||||
|
Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`.
|
||||||
|
|
||||||
|
## Целые блоки и дробные остатки
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||||
|
|
||||||
|
Независимый эталон Python Fraction: 160 пар с произведением внутри E27; целые
|
||||||
|
проверяются точно, дроби — с допуском 10^-12. Также границы int64/E27, сериализация,
|
||||||
|
рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися
|
||||||
|
долями. Все остальные тесты используют числовое сравнение и сериализацию через
|
||||||
|
`E`, а не сравнение ссылок на большие числа. Границы точности и новый формат
|
||||||
|
сохранения описаны в [документации арифметики](../world/owniverse/math/README.md).
|
||||||
|
|
||||||
|
## Модули симуляции
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd`
|
||||||
|
|
||||||
|
Проверяются отдельные экземпляры модулей и состояния для каждой вселенной, слабая обратная ссылка, освобождение модулей вместе с миром и защита от некорректного delta. Остальные тесты используют прямые API batches, ledger, production, automatic, stellar, life, environment и evolution; временные методы-переадресации Owniverse удалены. Описание границ модулей и порядка фаз: `../world/owniverse/README.md`.
|
||||||
|
|
||||||
|
## Математика распределений и каталог
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
||||||
|
|
||||||
|
Проверяются целочисленность и границы распределения, сохранение суммы, разрешение равных остатков, накопление дробей и долгов округления, нормализация весов, размер производственной партии, загрузка каталога и его индексы. Архитектура описана в `../world/owniverse/README.md`. Сохранение игрока не используется.
|
||||||
|
|
||||||
|
## Иконки стадий жизни
|
||||||
|
|
||||||
|
Тест также проверяет модуль `item_presentation.gd`: отсутствие изменений входного состояния и списка закрытых иконок, решения по разблокировке, отложенную и принудительную публикацию количеств.
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
||||||
|
|
||||||
|
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
||||||
|
|
||||||
|
## Параметры мира и индикаторы
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_world_indicators.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверяются сроки жизни существующих и новых звёзд при смене горючести, вероятности и мощность взрывов, влияние агрессии/порядка/просвещения на жизнь, накопление тенденций по времени и этикам, несколько событий за шаг, продолжение после сериализации, валидация сохранения, границы шкал и описания на двух языках. Сцена использует фикстуру без доступа к сохранению игрока.
|
||||||
|
|
||||||
|
## Прямой расчёт завершившихся сверхновых
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_short_lived_events.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Сравниваются ускоренный путь и прежний путь с созданием всех партий, отключаемый только в тестовой фикстуре. Проверяются LRG, HRG, RSG и RHG на границах коротких и длинных интервалов, существующие SN, нехватка энергии, ресурсы, бласты, привязки, остатки округления и живые партии. Дополнительно проверяется, что длинный интервал создаёт не более двух живых окон и одну запись завершений, сохраняя общее количество. Сохранение игрока этот тест не использует.
|
||||||
|
|
||||||
|
`test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`.
|
||||||
|
|
||||||
|
## Фоновое сохранение и частота обновления интерфейса
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_async_save.gd
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_ui_updates.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Сохранение: основной поток создаёт глубокий снимок состояния и параметров. Рабочий поток проверяет данные, записывает зашифрованный временный файл, проверяет чтением и заменяет основное сохранение; диагностический JSON создаётся из того же снимка. Одновременно работает один писатель. Для каждого имени файла хранится только последний ожидающий запрос. `request_save()` подтверждает постановку в очередь; результат записи сообщает сигнал `save_completed`. При закрытии игра ждёт `saves_drained`, при приостановке приложения и уничтожении менеджера очередь завершается через `flush_saves()`. Копирование снимка остаётся в основном потоке.
|
||||||
|
|
||||||
|
Тест сохранений пишет только в уникальную временную папку `/tmp`, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения `Invalid save`, `Save blocked` и `Cannot write` в этом тесте ожидаемы.
|
||||||
|
|
||||||
|
Тест интерфейса проверяет передачу количеств из presentation в контролы, очистку очереди после публикации, отложенное и принудительное обновление, производство по нажатию и формат времени. Stage в текущей реализации применяет очередь счётчиков и время каждый кадр; интервал 0,1 секунды используется для индикаторов. Рефакторинг этот порядок не меняет. Тест использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
||||||
|
|
||||||
|
## Время: шаг 0,1 игрового года
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_time.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Команда выполняется из общей папки проектов. Тесты используют искусственные состояния; сохранение игрока не читается и не перезаписывается.
|
||||||
|
|
||||||
|
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
|
||||||
|
|
||||||
|
Схема состояния `0.3.0` хранит:
|
||||||
|
|
||||||
|
- `time.datetime` — накопленный возраст, включая необработанную дробную часть;
|
||||||
|
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
|
||||||
|
|
||||||
|
Сохранения используют формат 4 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
|
||||||
|
|
||||||
|
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 4. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
|
||||||
|
|
||||||
|
## Согласованные правила эволюции
|
||||||
|
|
||||||
|
Сохраняются постепенные интервальные окна GI: начало преобразования — через `lifespan`, длительность окна — `lifespan / 40` при `lifespan > 10`, иначе `lifespan`.
|
||||||
|
|
||||||
|
Вероятностные исходы используют накопление дробного остатка GI, без случайного округления. Этот же подход выбран для будущего создания планет. Шансы планет записаны долями (`0.01` = 1%), а альтернативные исходы звёзд — процентами (`%0.1` = 0.1%). Парсер планет сохраняет дроби и поддерживает один элемент без `&`.
|
||||||
|
|
||||||
|
Уязвимость к бластам читается из `blastability` таблицы GI: HP, EON и цивилизации до C1.2 включительно; C1.3 и выше неуязвимы. Это ограничение относится к бластам уровня 1; локальное уничтожение планет уровня 2 в Unity охватывает все планеты.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_stellar_rules.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверяются парсер планет, граница уязвимости, накопление редких исходов, сериализация остатков и постепенное преобразование реальной партии RD в TWD.
|
||||||
|
|
||||||
|
Создание планет подключено к ручному и автоматическому производству звёзд. Бласты обрабатываются как события, а не ресурс. Выжившие планеты переходят преемнику звезды пропорционально числу реально полученных преемников; при отсутствии преемника освобождаются. Локальное разрушение выполняется перед передачей.
|
||||||
|
|
||||||
|
Дополнительно проверяются частичная передача, альтернативные преемники, уровни разрушения 1/2, накопление слабых бластов, сохранения, ручное/автоматическое создание планет и несколько поколений за длинный шаг. Правила и формулы описаны в `../simulation_rules.md`.
|
||||||
|
|
||||||
|
## Описания выбранных объектов
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_descriptions.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Источник текста — `Global.Templates[Global.Locale]`. Заголовок берётся из `Name`, основной текст — из `Description`. Поддерживаются ключи с классом (`RD.Entity.Description`) и старые ключи без класса (`RD.Description`).
|
||||||
|
|
||||||
|
`%valueN%` заменяется значением переменной, имя которой записано в поле `valueN` этой строки. Поддерживаются `amount`, `colored costs`, `lifespan`, `voids`, `he / h`, `metallicity`, показатели энергии, плотности и скорости времени. `amount1`–`amount4` сил пока отображаются как `—`; неизвестные непустые обозначения тоже дают `—`, пустые поля — пустую строку. `[p]` превращается в разделение абзацев, цветовые BBCode-теги сохраняются.
|
||||||
|
|
||||||
|
Доступные требования зелёные, недоступные красные. Для объектов со связанными экземплярами выводится доступное количество и серым общее; для планет — общее количество. Скорость изменения вычисляется по реальному времени и обновляется раз в 0,25 секунды. Доли He/H вычисляются из текущих свободных запасов; при пустой смеси показывается `—`. Металличность и плотность используют расчёт среды GI. Счётчики исходной материи и этики Unity в текущей GI отсутствуют и здесь не добавляются.
|
||||||
|
|
||||||
|
## Первичная жизнь
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_life.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверяются абиогенез, деградация, повторные попытки без создания дополнительных планет, появление C0.0 на прежней звезде, остатки вероятностей, сохранение/продолжение, однократная инициализация старых планет и отсутствие воскрешения после бластов.
|
||||||
|
|
||||||
|
Проверка загрузки этических правил, распределений и отделения космических цивилизаций:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_civilization_state.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Тест цивилизаций дополнительно проверяет общий звёздный бюджет, повторное использование резерва при переходе, звёзды в структурах, массовый эквивалент SMBH, блокировку неподдержанного прогресса, каскад деградации и повторное применение ограничений без потерь.
|
||||||
|
|
||||||
|
|
||||||
|
## Полная эволюция цивилизаций
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/test_civilization_evolution.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
|
||||||
|
|
||||||
|
## Профиль цивилизаций на шестой скорости
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- frames=20 profile
|
||||||
|
godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui scenario=mixed frames=10 profile
|
||||||
|
```
|
||||||
|
|
||||||
|
Сценарии `mixed`, `large`, `fragmented` заполняют все стадии и этики, синхронизируя
|
||||||
|
переходы; `large` использует по 6e20 цивилизаций, `fragmented` — десять партий.
|
||||||
|
Это искусственные стрессовые сценарии. `scenario=stable` использует штатные
|
||||||
|
сроки стадий. Параметр `snapshot=/tmp/civil` сохраняет состояние каждого сценария
|
||||||
|
для сравнения до и после изменения. UI запускает настоящую сцену со страницей
|
||||||
|
жизни и реальным delta, без загрузки и записи сохранения игрока.
|
||||||
|
|
||||||
|
`profile` выводит суммарные микросекунды и число вызовов по этапам. Вложенные
|
||||||
|
этапы входят во время родительских; складывать их нельзя. В игре сбор выключен.
|
||||||
|
Кэши физических количеств принадлежат отдельному миру и проверяют изменения
|
||||||
|
количеств, вложенных привязок и индексов каталога. Кэш структуры правил не входит
|
||||||
|
в сохранение: при изменении Claim/бонусов нужно перестроить индексы каталога,
|
||||||
|
при изменении home в существующей таблице — invalidate_civilization_layout().
|
||||||
|
|
||||||
|
После оптимизации на M4 Pro, по 20 шагов 1/60 секунды, среднее время:
|
||||||
|
`mixed` 295 → 235 мс, `large` 666 → 539 мс, `fragmented` 359 → 282 мс.
|
||||||
|
Полные состояния всех трёх сценариев после 20 шагов совпали до и после изменения.
|
||||||
|
Дополнительно тест распределений сравнивает новый путь с прежним на 300
|
||||||
|
случайных наборах, включая 1e40 и накопленные долги округления.
|
||||||
|
|
||||||
|
## Расчёт между кадрами
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd`
|
||||||
|
|
||||||
|
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||||
|
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||||
|
на главном потоке, состояние публикуется только после полного завершения.
|
||||||
|
Длительность одной транзакции ограничена обычным шагом 1/60 секунды выбранной
|
||||||
|
скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
|
||||||
|
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может
|
||||||
|
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.
|
||||||
|
|
||||||
|
Производство во время транзакции выполняется после её завершения. Запрошенное
|
||||||
|
время, активный интервал, скорость и очередь производства входят в сохранение;
|
||||||
|
при загрузке незавершённый интервал пересчитывается с опубликованного состояния.
|
||||||
|
Сброс мира отменяет отложенный расчёт. Тест проверяет точное совпадение непрерывного
|
||||||
|
и распределённого расчёта, очереди, переключение скорости, восстановление и отмену.
|
||||||
|
|
||||||
|
На M4 Pro, 180 кадров реального UI на шестой скорости: смешанный сценарий
|
||||||
|
15.9 FPS (максимальный кадр 98 мс), большие количества 17.5 FPS (106 мс),
|
||||||
|
много партий 16.3 FPS (97 мс).
|
||||||
|
Это FPS отрисовки, а не число завершённых транзакций симуляции в секунду.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var ui: bool = "ui" in OS.get_cmdline_user_args()
|
||||||
|
var scenarios: Array = ["mixed", "large", "fragmented"]
|
||||||
|
var frames: int = 120
|
||||||
|
var step_delta: float = 1.0 / 60.0
|
||||||
|
for argument in OS.get_cmdline_user_args():
|
||||||
|
if argument.begins_with("scenario="): scenarios = [argument.trim_prefix("scenario=")]
|
||||||
|
if argument.begins_with("delta="): step_delta = float(argument.trim_prefix("delta="))
|
||||||
|
if argument.begins_with("frames="): frames = maxi(2, int(argument.trim_prefix("frames=")))
|
||||||
|
Engine.max_fps = 60
|
||||||
|
if ui: DisplayServer.window_set_size(Vector2i(540, 960))
|
||||||
|
for scenario in scenarios:
|
||||||
|
var scene
|
||||||
|
var stage
|
||||||
|
var world
|
||||||
|
if ui:
|
||||||
|
scene = load("res://world/world.tscn").instantiate()
|
||||||
|
stage = scene.get_node("Stage")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||||
|
root.add_child(scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
world = stage.owniverse
|
||||||
|
else:
|
||||||
|
world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
Fixture.seed(world, root.get_node("Global"), scenario == "fragmented", scenario == "large", scenario == "stable")
|
||||||
|
world.profile_enabled = "profile" in OS.get_cmdline_user_args()
|
||||||
|
var samples: Array = []
|
||||||
|
if ui:
|
||||||
|
stage.stage_page_scroller.scroll_horizontal = 4 * stage.PAGE_WIDTH
|
||||||
|
for item in stage.owniverse_items.values(): item.visible = true
|
||||||
|
for page in stage.item_pages: page.visible = true
|
||||||
|
stage._set_item_description(stage.owniverse_items["C2.0"])
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
for warmup in 10: await process_frame
|
||||||
|
stage.measuring = true
|
||||||
|
for frame in frames:
|
||||||
|
await process_frame
|
||||||
|
if (frame + 1) % 10 == 0:
|
||||||
|
print("%s: measured %d UI frames" % [scenario, frame + 1])
|
||||||
|
stage.measuring = false
|
||||||
|
samples = stage.process_samples
|
||||||
|
report(scenario + " UI frame", stage.frame_samples)
|
||||||
|
else:
|
||||||
|
for frame in frames:
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(step_delta)
|
||||||
|
samples.append((Time.get_ticks_usec() - started) / 1000.0)
|
||||||
|
report(scenario + (" UI handler" if ui else " CPU"), samples)
|
||||||
|
if ui:
|
||||||
|
print("Completed simulation transactions: ", world.frame_commits, "; published year: ", world.state.time.year, "; requested year: ", world.frame_clock.get("year", 0))
|
||||||
|
print("Statistics ", world.state.civilization_stats)
|
||||||
|
if world.profile_enabled: print("Profile microseconds/calls: ", world.profile)
|
||||||
|
for argument in OS.get_cmdline_user_args():
|
||||||
|
if argument.begins_with("snapshot="):
|
||||||
|
var file = FileAccess.open(argument.trim_prefix("snapshot=") + "-" + scenario + ".json", FileAccess.WRITE)
|
||||||
|
file.store_string(JSON.stringify(world.E.encode(world.state)))
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
if ui: scene.free()
|
||||||
|
quit()
|
||||||
|
|
||||||
|
func report(label: String, samples: Array):
|
||||||
|
var total: float = 0.0
|
||||||
|
for sample in samples: total += sample
|
||||||
|
samples.sort()
|
||||||
|
print("%s: mean %.2f ms, p95 %.2f ms, max %.2f ms, %.1f per second" % [label, total / samples.size(), samples[ceili(samples.size() * 0.95) - 1], samples.back(), samples.size() * 1000 / total])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b5w4sp6eepypa
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Read-only benchmark. Loads a diagnostic JSON, never invokes SaveManager.
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func restore_json_integers(value):
|
||||||
|
# JSON parses ordinary integer tokens as float. Large integers are $exact.
|
||||||
|
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
|
||||||
|
return int(value)
|
||||||
|
if value is Dictionary:
|
||||||
|
for key in value: value[key] = restore_json_integers(value[key])
|
||||||
|
elif value is Array:
|
||||||
|
for i in value.size(): value[i] = restore_json_integers(value[i])
|
||||||
|
return value
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var args = OS.get_cmdline_user_args()
|
||||||
|
if args.is_empty():
|
||||||
|
print("Usage: --script tests/benchmark_saved_state.gd -- /path/to/save.adventure.json [delta_seconds]")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
|
||||||
|
if not data is Dictionary or data.get("format_version") != 5:
|
||||||
|
push_error("Expected diagnostic JSON format 5")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var initial: Dictionary = E.decode(restore_json_integers(data)).state
|
||||||
|
var delta: float = args[1].to_float() if args.size() > 1 else 1.0 / 60.0
|
||||||
|
if not is_finite(delta) or delta <= 0:
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
for speed in [1, 4, 6]:
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
world.state = initial.duplicate(true)
|
||||||
|
world.speed = speed
|
||||||
|
var elapsed: Array = []
|
||||||
|
for cycle in 6:
|
||||||
|
var start: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(delta)
|
||||||
|
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
|
||||||
|
print("Speed %d, six consecutive %s-second steps, ms: %s" % [speed, delta, elapsed])
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dji1apht4pn5y
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
|
||||||
|
func run():
|
||||||
|
for scenario in ["ordinary", "hypergiants"]:
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
var group: String = world.catalog.entities[id].group_as
|
||||||
|
if group != "":
|
||||||
|
world.state.amount[group] = 0
|
||||||
|
world.state.stats.total_amount[group] = 0
|
||||||
|
world.speed = 6
|
||||||
|
world.state.amount.S = E.of("1e24")
|
||||||
|
world.state.amount.H = E.of("1e24")
|
||||||
|
for entry in [["RD", 1234567], ["YD", 765432], ["STN", 654321]]:
|
||||||
|
world.state.amount[entry[0]] = entry[1]
|
||||||
|
world.batches.append_batch(world.state, entry[0], entry[1], 0, 0)
|
||||||
|
if scenario == "hypergiants":
|
||||||
|
world.state.amount.HG = E.of("1e20")
|
||||||
|
world.batches.append_batch(world.state, "HG", E.of("1e20"), 0, 0)
|
||||||
|
var start_year = E.subtract(world.catalog.entities.HG.lifespan, 1000)
|
||||||
|
world.state.time = {"datetime": start_year, "year": start_year}
|
||||||
|
var samples: Array = []
|
||||||
|
var total: int = 0
|
||||||
|
for frame in 600:
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(1.0 / 60.0)
|
||||||
|
var duration: int = Time.get_ticks_usec() - started
|
||||||
|
samples.append(duration)
|
||||||
|
total += duration
|
||||||
|
samples.sort()
|
||||||
|
print("Speed 6 %s: 600 frames, mean %.3f ms, p95 %.3f ms, max %.3f ms, %.1f cycles/sec" % [scenario, total / 600000.0, samples[569] / 1000.0, samples.back() / 1000.0, 600000000.0 / total])
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bboo6op14es24
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
|
||||||
|
func report(label: String, samples: Array):
|
||||||
|
var total: float = 0.0
|
||||||
|
var slow: int = 0
|
||||||
|
for sample in samples:
|
||||||
|
total += sample
|
||||||
|
if sample > 33.333: slow += 1
|
||||||
|
samples.sort()
|
||||||
|
print("%s: %d samples, mean %.3f ms, p95 %.3f ms, max %.3f ms, >33ms %d" % [label, samples.size(), total / samples.size(), samples[int(samples.size() * 0.95) - 1], samples.back(), slow])
|
||||||
|
|
||||||
|
func run():
|
||||||
|
Engine.max_fps = 60
|
||||||
|
DisplayServer.window_set_size(Vector2i(540, 960))
|
||||||
|
print("Renderer: ", RenderingServer.get_current_rendering_method(), " adapter: ", RenderingServer.get_video_adapter_name())
|
||||||
|
for scenario in ["ordinary", "hypergiants"]:
|
||||||
|
for page in ([2] if "stars" in OS.get_cmdline_user_args() else [1, 2]):
|
||||||
|
var scene = load("res://world/world.tscn").instantiate()
|
||||||
|
var stage = scene.get_node("Stage")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||||
|
root.add_child(scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var world = stage.owniverse
|
||||||
|
world.speed = 6
|
||||||
|
world.state.amount.S = E.of("1e24")
|
||||||
|
world.state.amount.H = E.of("1e24")
|
||||||
|
for entry in [["RD", 1234567], ["YD", 765432], ["STN", 654321]]:
|
||||||
|
world.state.amount[entry[0]] = entry[1]
|
||||||
|
world.batches.append_batch(world.state, entry[0], entry[1], 0, 0)
|
||||||
|
if scenario == "hypergiants":
|
||||||
|
world.state.amount.HG = E.of("1e20")
|
||||||
|
world.batches.append_batch(world.state, "HG", E.of("1e20"), 0, 0)
|
||||||
|
var start_year = E.subtract(world.catalog.entities.HG.lifespan, 1000)
|
||||||
|
world.state.time = {"datetime": start_year, "year": start_year}
|
||||||
|
stage.stage_page_scroller.scroll_horizontal = page * stage.PAGE_WIDTH
|
||||||
|
for item in stage.owniverse_items.values(): item.visible = true
|
||||||
|
if page == 2: stage._control_page(stage.owniverse_items.HG)
|
||||||
|
stage._set_item_description(stage.owniverse_items.HG if page == 2 else stage.owniverse_items.H)
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
for warmup in 30: await process_frame
|
||||||
|
stage.measuring = true
|
||||||
|
for frame in 600: await process_frame
|
||||||
|
stage.measuring = false
|
||||||
|
var label: String = "%s page %d" % [scenario, page]
|
||||||
|
report(label + " Stage CPU", stage.process_samples)
|
||||||
|
report(label + " frame wall", stage.frame_samples)
|
||||||
|
print("UI counters: H=", stage.owniverse_items.H.description.text, " HG=", stage.owniverse_items.HG.description.text, " date=", stage.date_time.text)
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
scene.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b3gmlo2vq6kk4
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
extends "res://stage/stage.gd"
|
||||||
|
|
||||||
|
var measuring: bool = false
|
||||||
|
var process_samples: Array = []
|
||||||
|
var frame_samples: Array = []
|
||||||
|
var previous_frame: int = 0
|
||||||
|
|
||||||
|
# Exercise the production Stage, with isolated state and no player-save I/O.
|
||||||
|
func load_data() -> bool: return false
|
||||||
|
func save_data() -> void: pass
|
||||||
|
func _notification(_what: int) -> void: pass
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not measuring: return
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
if previous_frame: frame_samples.append((started - previous_frame) / 1000.0)
|
||||||
|
previous_frame = started
|
||||||
|
super._process(delta)
|
||||||
|
process_samples.append((Time.get_ticks_usec() - started) / 1000.0)
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://d1wm7nav71y3o
|
||||||
+1
File diff suppressed because one or more lines are too long
+30
@@ -0,0 +1,30 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
static func seed(world, global, fragmented: bool = false, large: bool = false, stable: bool = false):
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
var entity = world.catalog.entities[id]
|
||||||
|
if entity.group_as != "":
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
for id in ["RD", "BG", "TWD", "SNS", "MGT", "BH25", "SMBH", "SCD", "GEM", "LGG", "LSC", "LGF", "LGW", "LUB"]:
|
||||||
|
world.state.amount[id] = E.of("1e48")
|
||||||
|
world.state.amount.S = E.of("1e50")
|
||||||
|
world.state.amount.H = E.of("1e48")
|
||||||
|
world.state.binds.RD = {"HP": E.of("1e30"), "RP": E.of("1e30"), "GG": E.of("1e30"), "AB": E.of("1e30")}
|
||||||
|
var count = E.of("6e20") if large else 6000
|
||||||
|
for id in global.CivilizationRules:
|
||||||
|
if E.le(global.CivilizationRules[id].span, 0): continue
|
||||||
|
world.state.amount[id] = count
|
||||||
|
world.state.ethics[id] = {}
|
||||||
|
for ethic in ["m", "l", "w", "d", "c", "b"]:
|
||||||
|
world.state.ethics[id][ethic] = E.divide(count, 6)
|
||||||
|
world.state.batches[id] = []
|
||||||
|
var parties: int = 10 if fragmented else 1
|
||||||
|
for i in parties:
|
||||||
|
var lifespan = E.of(global.CivilizationRules[id].span) if stable else 1
|
||||||
|
world.state.batches[id].append({"amount": E.divide(count, parties), "created_from": i * 100, "created_to": 1000 + i * 100, "start_year": E.add(lifespan, i * 100), "end_year": E.add(lifespan, 1000 + i * 100)})
|
||||||
|
world.ledger.refresh_groups(world.state)
|
||||||
|
world.speed = 6
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bjloakx4qdpsj
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
+59
@@ -0,0 +1,59 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
# Frozen apportionment before the civilization performance refactor.
|
||||||
|
|
||||||
|
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||||
|
var total: Variant = 0
|
||||||
|
for amount in requests.values():
|
||||||
|
total = E.add(total, E.floor_value(E.maximum(0, amount)))
|
||||||
|
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
|
||||||
|
var result: Dictionary = {}
|
||||||
|
var order: Array = []
|
||||||
|
var assigned: Variant = 0
|
||||||
|
for id in requests:
|
||||||
|
var expected: Variant = (E.mul_div(E.floor_value(E.maximum(0, requests[id])), available, total) if E.gt(total, 0) else 0)
|
||||||
|
result[id] = E.floor_value(expected)
|
||||||
|
assigned = E.add(assigned, result[id])
|
||||||
|
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||||
|
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||||
|
for item : Dictionary in order:
|
||||||
|
if E.ge(assigned, available):
|
||||||
|
break
|
||||||
|
if E.lt(result[item.id], E.floor_value(requests[item.id])):
|
||||||
|
result[item.id] = E.add(result[item.id], 1)
|
||||||
|
assigned = E.add(assigned, 1)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func sum(pop: Dictionary) -> Variant:
|
||||||
|
var result: Variant = 0
|
||||||
|
for n in pop.values(): result = E.add(result, n)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
var sum_weights: Variant = sum(weights)
|
||||||
|
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
|
||||||
|
var expected: Dictionary = {}
|
||||||
|
var positive_sum: Variant = 0
|
||||||
|
for id in weights:
|
||||||
|
expected[id] = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||||
|
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||||
|
var order: Array = []
|
||||||
|
var assigned: Variant = 0
|
||||||
|
for id in weights:
|
||||||
|
var normalized: Variant = (E.mul_div(E.maximum(0, expected[id]), count, positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||||
|
result[id] = E.floor_value(normalized)
|
||||||
|
assigned = E.add(assigned, result[id])
|
||||||
|
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||||
|
order.sort_custom(func ( a , b ) : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||||
|
for entry in order:
|
||||||
|
if E.ge(assigned, count): break
|
||||||
|
result[entry.id] = E.add(result[entry.id], 1)
|
||||||
|
assigned = E.add(assigned, 1)
|
||||||
|
for id in weights:
|
||||||
|
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 1)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://dxsly3qpyqrjy
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
extends "res://world/owniverse/owniverse.gd"
|
||||||
|
|
||||||
|
# Compare with the materialized-batch path, without overriding the coordinator.
|
||||||
|
func _init() -> void:
|
||||||
|
stellar.direct_completions = false
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://cn6hhpjw1sjpb
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
extends "res://stage/stage.gd"
|
||||||
|
|
||||||
|
# Keep scene wiring and display logic, without accessing the player's save.
|
||||||
|
func load_data() -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
func save_data() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _notification(_what: int) -> void:
|
||||||
|
pass
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://c8ewikdhlvnex
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
var failures: int = 0
|
||||||
|
var checks: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(value: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not value):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var actions = load("res://ui/actions/actions.tscn").instantiate()
|
||||||
|
root.add_child(actions)
|
||||||
|
for maximum in [0, E.of("0.1"), E.of("0.99"), 1, E.of("1.9"), 10, 100, 123, 1000, 10000, E.of("1e9"), E.of("1.1e9"), E.of("9e27")]:
|
||||||
|
actions.set_actions("test", maximum)
|
||||||
|
var labels: Array = []
|
||||||
|
var values: Array = []
|
||||||
|
for i in 5:
|
||||||
|
var value: Variant = actions.action_values[i]
|
||||||
|
var text: String = actions.action_labels[i].text
|
||||||
|
check((E.eq(value, E.floor_value(value)) and E.le(value, E.floor_value(maximum))), "Whole affordable request")
|
||||||
|
if E.gt(value, 0):
|
||||||
|
check(((not values.has(value)) and (not labels.has(text))), "Unique requests and displayed labels")
|
||||||
|
values.append(value)
|
||||||
|
labels.append(text)
|
||||||
|
else:
|
||||||
|
check((E.eq(text, "") and actions.action_labels[i].get_parent().get_node("TextureButton").disabled), "Unused button blank and disabled")
|
||||||
|
if E.ge(maximum, 1): check(E.eq(values.back(), E.floor_value(maximum)), "Maximum retained")
|
||||||
|
actions.set_actions("test", 100)
|
||||||
|
check(E.eq(actions.action_values.values(), [1, 10, 100, 0, 0]), "Exact power of ten not duplicated")
|
||||||
|
actions.set_actions("SMBH", 25, func ( _request ) : return 25)
|
||||||
|
check(E.eq(actions.action_values.values(), [25, 0, 0, 0, 0]), "Identical SMBH previews collapse to one button")
|
||||||
|
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
|
||||||
|
check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted")
|
||||||
|
check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained")
|
||||||
|
var emitted: Array = []
|
||||||
|
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
|
||||||
|
actions._on_action_button_pressed(2)
|
||||||
|
actions._on_action_button_pressed(4)
|
||||||
|
check(E.eq(emitted, [123]), "Only active button emits its matching request")
|
||||||
|
actions.queue_free()
|
||||||
|
print(("Action values: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dilbcphuku6k1
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
var failures: int = 0
|
||||||
|
var checks: int = 0
|
||||||
|
var completions: Array = []
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(value: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not value):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var manager = load("res://autoload/save_manager.gd").new()
|
||||||
|
var directory: String = ("/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()])
|
||||||
|
DirAccess.make_dir_recursive_absolute(directory)
|
||||||
|
manager.save_directory = directory
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.save_completed.connect(func ( filename , success , error ) : completions.append([filename, success, error]))
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.state.energy = E.subtract(world.state.energy, 1)
|
||||||
|
world.state.time = preload("res://world/owniverse/simulation_time.gd").advance({"datetime": E.of("1e27")}, 0.000000001, 1)
|
||||||
|
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
||||||
|
var initial_energy: Variant = world.state.energy
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
var old_plaintext: bool = global.save_plaintext_copy
|
||||||
|
global.save_plaintext_copy = true
|
||||||
|
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
|
||||||
|
data.state.energy = 99
|
||||||
|
data.ui.current_page = 2
|
||||||
|
var frames: int = 0
|
||||||
|
var deadline: int = E.add(Time.get_ticks_msec(), 10000)
|
||||||
|
while (manager.is_saving() and E.lt(Time.get_ticks_msec(), deadline)):
|
||||||
|
frames = E.add(frames, 1)
|
||||||
|
await process_frame
|
||||||
|
check(((not manager.is_saving()) and E.gt(frames, 0)), "Frames continue while save is pending")
|
||||||
|
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
||||||
|
check((E.eq(saved.state.energy, initial_energy) and E.eq(saved.ui.current_page, 1)), "Worker owns a deep snapshot")
|
||||||
|
check(E.eq(saved.state.time, world.state.time), "Encrypted save preserves exact ticks at huge age")
|
||||||
|
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
||||||
|
check(E.eq(E.decode(plain.state.energy), initial_energy), "Diagnostic JSON uses same snapshot")
|
||||||
|
check(E.eq(E.decode(plain.state.time), saved.state.time), "Diagnostic JSON preserves fractional time")
|
||||||
|
check((E.eq(completions.size(), 1) and completions[0][1]), "Completion reports successful write")
|
||||||
|
|
||||||
|
completions.clear()
|
||||||
|
data.state.energy = 1
|
||||||
|
manager.request_save("latest", "test-password", data)
|
||||||
|
data.state.energy = 2
|
||||||
|
manager.request_save("latest", "test-password", data)
|
||||||
|
data.state.energy = 3
|
||||||
|
manager.request_save("latest", "test-password", data)
|
||||||
|
data.state.energy = 4
|
||||||
|
manager.flush_saves()
|
||||||
|
check(E.eq(completions.size(), 2), "Repeated requests coalesce behind active write")
|
||||||
|
check(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Last queued snapshot wins")
|
||||||
|
|
||||||
|
var pending_clock: Dictionary = preload("res://world/owniverse/simulation_time.gd").advance(world.state.time, 0.01, 1)
|
||||||
|
data.simulation = {"requested_clock": pending_clock, "active_clock": pending_clock, "speed": 1, "productions": [{"id": "H", "amount": 7, "resource": true}]}
|
||||||
|
manager.request_save("checkpoint", "test-password", data)
|
||||||
|
manager.flush_saves()
|
||||||
|
check(E.eq(manager.load_data("checkpoint", "test-password").simulation, data.simulation), "Encrypted save preserves requested time and queued production")
|
||||||
|
var malformed: Dictionary = manager._snapshot("probe", "test-password", data).payload
|
||||||
|
malformed.simulation.productions[0].id = "missing-entity"
|
||||||
|
check(manager.validate_data(malformed).contains("queued production"), "Unknown queued production is rejected before resume")
|
||||||
|
malformed = manager._snapshot("probe", "test-password", data).payload
|
||||||
|
malformed.simulation.requested_clock.year = -1
|
||||||
|
check(manager.validate_data(malformed).contains("simulation clock"), "Invalid requested clock is rejected before resume")
|
||||||
|
data.erase("simulation")
|
||||||
|
|
||||||
|
var incompatible_path: String = directory.path_join("incompatible.adventure")
|
||||||
|
var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password")
|
||||||
|
incompatible.store_var({"format_version": E.neg(1), "state": {"unknown": true}}, false)
|
||||||
|
incompatible.close()
|
||||||
|
var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path)
|
||||||
|
var previous_archive = FileAccess.open(E.add(incompatible_path, ".incompatible.bak"), FileAccess.WRITE)
|
||||||
|
previous_archive.store_string("keep previous backup")
|
||||||
|
previous_archive.close()
|
||||||
|
manager.blocked_saves.incompatible = true
|
||||||
|
check((manager.load_data("incompatible", "test-password").is_empty() and E.eq(manager.last_error, "")), "Unsupported format permits a fresh session")
|
||||||
|
check(((not manager.blocked_saves.has("incompatible")) and (not FileAccess.file_exists(incompatible_path))), "Incompatible file no longer blocks saving")
|
||||||
|
check(E.eq(FileAccess.get_file_as_bytes(E.add(incompatible_path, ".incompatible.bak.1")), original), "Unsupported save archived without losing bytes")
|
||||||
|
check(E.eq(FileAccess.get_file_as_string(E.add(incompatible_path, ".incompatible.bak")), "keep previous backup"), "Earlier archive preserved")
|
||||||
|
check(manager.request_save("incompatible", "test-password", data), "New session can queue a save")
|
||||||
|
manager.flush_saves()
|
||||||
|
check(E.eq(manager.load_data("incompatible", "test-password").state, data.state), "New format saves and reloads after incompatible file")
|
||||||
|
|
||||||
|
# Expected failures below must leave the last good encrypted file intact.
|
||||||
|
data.state.energy = E.neg(1)
|
||||||
|
manager.request_save("latest", "test-password", data)
|
||||||
|
manager.flush_saves()
|
||||||
|
check(((not completions.back()[1]) and manager.last_error.contains("Invalid save")), "Worker validation error reaches main thread")
|
||||||
|
check(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Invalid snapshot preserves previous save")
|
||||||
|
manager.blocked_saves.latest = true
|
||||||
|
check((not manager.request_save("latest", "test-password", data)), "Unreadable save protection also applies to queue")
|
||||||
|
# A valid snapshot exercises an actual I/O failure, not validation.
|
||||||
|
data.state.energy = 5
|
||||||
|
manager.request_save("missing/failure", "test-password", data)
|
||||||
|
manager.flush_saves()
|
||||||
|
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
|
||||||
|
|
||||||
|
manager.request_save("shutdown", "test-password", data)
|
||||||
|
data.state.energy = 6
|
||||||
|
manager.request_save("shutdown", "test-password", data)
|
||||||
|
manager.free()
|
||||||
|
var reader = load("res://autoload/save_manager.gd").new()
|
||||||
|
reader.save_directory = directory
|
||||||
|
check(E.eq(reader.load_data("shutdown", "test-password").state.energy, 6), "Teardown drains final pending snapshot")
|
||||||
|
reader.free()
|
||||||
|
world.free()
|
||||||
|
# Exercise the stage entry point too: manager tests alone do not catch a
|
||||||
|
# regression back to the synchronous API in stage.gd.
|
||||||
|
var shared_manager: Node = root.get_node("SaveManager")
|
||||||
|
var original_directory: String = shared_manager.save_directory
|
||||||
|
shared_manager.save_directory = directory
|
||||||
|
var stage = load("res://stage/stage.gd").new()
|
||||||
|
stage.pending_page = 2
|
||||||
|
stage.owniverse.state.energy = 42
|
||||||
|
stage.save_data()
|
||||||
|
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
|
||||||
|
stage.owniverse.state.energy = 43
|
||||||
|
shared_manager.flush_saves()
|
||||||
|
check(E.eq(shared_manager.load_data("owniverse", "qwerty").state.energy, 42), "Stage save writes an independent snapshot")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.free()
|
||||||
|
shared_manager.save_directory = original_directory
|
||||||
|
global.save_plaintext_copy = old_plaintext
|
||||||
|
for filename in DirAccess.get_files_at(directory):
|
||||||
|
DirAccess.remove_absolute(directory.path_join(filename))
|
||||||
|
DirAccess.remove_absolute(directory)
|
||||||
|
print(("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dsn0ih3yfrry2
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
world.state.batch_origins.LGRB = E.of("10357034.80000000074505806")
|
||||||
|
var boundary: Variant = E.of("41215289.79999999701976776")
|
||||||
|
for interval in [[E.subtract(boundary, E.of("2.5")), E.add(boundary, E.of("2.25"))], [boundary, E.add(boundary, 2)]]:
|
||||||
|
world.state.batches.LGRB = []
|
||||||
|
world.batches.append_batch(world.state, "LGRB", 100, interval[0], interval[1])
|
||||||
|
var total: Variant = 0
|
||||||
|
var last: Variant = E.neg(INF)
|
||||||
|
for batch : Dictionary in world.state.batches.LGRB:
|
||||||
|
total = E.add(total, batch.amount)
|
||||||
|
check(E.gt(batch.created_from, last), "Batch windows advance monotonically")
|
||||||
|
check(E.gt(batch.created_to, batch.created_from), "Positive window duration")
|
||||||
|
check(E.ge(batch.amount, 0), "No negative batch amounts")
|
||||||
|
last = batch.created_from
|
||||||
|
check(E.eq(total, 100), "Boundary correction preserves all objects")
|
||||||
|
check(E.le(world.state.batches.LGRB.size(), 7), "Finite number of windows")
|
||||||
|
dispose(world)
|
||||||
|
print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dq86k8bto42xl
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void:
|
||||||
|
world.state.amount.N = 0
|
||||||
|
world.state.amount[id] = count
|
||||||
|
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
|
||||||
|
func sum_ethics(world, id: String) -> Variant:
|
||||||
|
var result: Variant = 0
|
||||||
|
for n in world.state.ethics.get(id, {}).values(): result = E.add(result, n)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func invariants(world) -> void:
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
for id in world.state.ethics:
|
||||||
|
check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id))
|
||||||
|
for n in world.state.ethics[id].values():
|
||||||
|
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
|
||||||
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
||||||
|
var world = fixture()
|
||||||
|
seed_population(world, "C0.2", 60000)
|
||||||
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
|
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses")
|
||||||
|
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades")
|
||||||
|
check(E.eq(world.state.civilization_stats.get("wars", 0), 0), "Table disables early wars")
|
||||||
|
check(E.eq(E.add(E.add(world.state.amount["C0.2"], world.state.amount["C0.3"]), world.state.amount["C0.1"]), 60000), "Ordinary evolution conserves population")
|
||||||
|
check(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations")
|
||||||
|
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics")
|
||||||
|
invariants(world)
|
||||||
|
var restored = fixture()
|
||||||
|
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
|
world.evolution.evolve_batches(world.state, 2100)
|
||||||
|
restored.evolution.evolve_batches(restored.state, 2100)
|
||||||
|
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution")
|
||||||
|
invariants(world)
|
||||||
|
dispose(restored)
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 100000000
|
||||||
|
seed_population(world, "C2.0", 60000)
|
||||||
|
world.state.amount["C1.9"] = 10000
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
|
check(E.gt(world.state.civilization_stats.wars, 0), "Wars execute when enabled")
|
||||||
|
check(E.gt(world.state.civilization_stats.collateral, 0), "Wars affect previous-stage populations")
|
||||||
|
check(E.gt(world.state.amount["C2.1"], 0), "Winners and peaceful progress reach next stage")
|
||||||
|
invariants(world)
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 1
|
||||||
|
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5}
|
||||||
|
seed_population(world, "C1.6", 60000)
|
||||||
|
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
||||||
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
|
var after: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
check(E.eq(world.state.civilization_stats.colonies, 20), "Colonization limited to real targets")
|
||||||
|
check(E.eq(E.add(E.add(E.add(after.HP, after.GG), after.AB), after.RP), 0), "Colonized objects consumed once")
|
||||||
|
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level")
|
||||||
|
var totals: Variant = 0
|
||||||
|
for id in after:
|
||||||
|
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id])
|
||||||
|
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds")
|
||||||
|
invariants(world)
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
global.CivilizationRules = baseline.duplicate(true)
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = E.of("1e25")
|
||||||
|
world.state.amount.BG = E.of("1e25")
|
||||||
|
world.state.amount.TWD = E.of("1e25")
|
||||||
|
world.state.amount.SNS = E.of("1e25")
|
||||||
|
world.state.amount.MGT = E.of("1e25")
|
||||||
|
world.state.amount.BH25 = E.of("1e25")
|
||||||
|
world.state.amount.SMBH = E.of("1e26")
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
if E.ne(world.catalog.entities[id].claim, ""): world.state.amount[id] = E.of("1e12")
|
||||||
|
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
|
||||||
|
for id in global.CivilizationRules:
|
||||||
|
if E.eq(id, "Ct"): continue
|
||||||
|
global.CivilizationRules[id].fights = false
|
||||||
|
global.CivilizationRules[id].colonizes = false
|
||||||
|
global.CivilizationRules[id].diplomacy = false
|
||||||
|
for row : Dictionary in global.CivilizationRules[id].ethics.values():
|
||||||
|
row.progress = "1"
|
||||||
|
row.degradation = "0"
|
||||||
|
world.catalog.entities[id].lifespan = 1
|
||||||
|
world.catalog.entities[id].lifespan_delta = 1
|
||||||
|
global.CivilizationRules[id].span = 1
|
||||||
|
seed_population(world, "C0.2", 6)
|
||||||
|
world.evolution.evolve_batches(world.state, 1000)
|
||||||
|
check(E.eq(world.state.amount.Ct, 6), "Complete C0.2 -> C4.4 -> Ct chain runs")
|
||||||
|
check((not world.state.batches.has("Ct")), "Transcendence is terminal")
|
||||||
|
invariants(world)
|
||||||
|
dispose(world)
|
||||||
|
global.CivilizationRules = baseline
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 10000
|
||||||
|
world.state.amount.SCS = 1
|
||||||
|
world.state.amount["C2.4"] = 1
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
var span: Variant = world.state.batches["C2.4"][0].start_year
|
||||||
|
check(E.near(span, E.divide(20000, E.of("3.6"))), "Structure bonus shortens newly registered lifespan")
|
||||||
|
dispose(world)
|
||||||
|
var evolution = load("res://world/owniverse/civilization_evolution.gd")
|
||||||
|
var balanced: Dictionary = {"m": 10000, "l": 10000, "w": 10000, "d": 10000, "c": 10000, "b": 10000}
|
||||||
|
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
|
||||||
|
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
|
||||||
|
check(E.eq(neutral, balanced), "Ordinary drift preserves balanced ethics")
|
||||||
|
check(E.gt(diplomatic.d, diplomatic.w), "Diplomacy uses per-ethic influence")
|
||||||
|
check(E.eq(evolution.total(diplomatic), 60000), "Diplomacy conserves population")
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = E.of("1e15")
|
||||||
|
world.state.amount.BG = E.of("1e15")
|
||||||
|
world.state.amount["C3.0"] = 6
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check((E.eq(world.state.amount["C3.0"], 0) and E.eq(world.state.amount["C2.9"], 6)), "Missing mandatory structures degrade existing civilizations")
|
||||||
|
invariants(world)
|
||||||
|
dispose(world)
|
||||||
|
print(("Civilization evolution: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://u4ujq17gu2l5
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
check(E.eq(global.CivilizationRules.size(), 44), "Load all ethical civilization stages")
|
||||||
|
for row : Dictionary in global.CivilizationRules.values():
|
||||||
|
check((E.eq(row.ethics.size(), 6) or E.eq(row.span, 0)), "Every ethical stage has six rules")
|
||||||
|
world.state.amount.RD = 12
|
||||||
|
world.state.amount["C0.2"] = 8
|
||||||
|
world.state.binds.RD = {"C2.0": 12, "C1.9": 3}
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check((E.eq(world.state.amount["C2.0"], 12) and E.eq(world.state.binds.RD["C2.0"], 0)), "Old space civilizations detach without loss")
|
||||||
|
check(E.eq(world.state.binds.RD["C1.9"], 3), "Planetary civilizations retain their host")
|
||||||
|
check(E.eq(world.state.ethics["C0.2"], {"m": 2, "l": 2, "w": 1, "d": 1, "c": 1, "b": 1}), "Migration uses equal ethics and deterministic remainder")
|
||||||
|
check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.eq(n, 2)), "Bound civilization ethics count physical population")
|
||||||
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state, before), "Repeated initialization preserves ethics")
|
||||||
|
world.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0}
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained")
|
||||||
|
world.state.amount["C0.2"] = 3
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
|
||||||
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||||
|
var restored = fixture()
|
||||||
|
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
|
restored.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(restored.state, world.state), "Save roundtrip preserves ethics and ownership")
|
||||||
|
dispose(restored)
|
||||||
|
dispose(world)
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 10
|
||||||
|
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||||
|
row.progress = "1"
|
||||||
|
row.degradation = "0"
|
||||||
|
row.colonization = "0"
|
||||||
|
world.state.binds.RD = {"C1.9": 10}
|
||||||
|
world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}]
|
||||||
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
|
check((E.eq(world.state.amount["C2.0"], 10) and E.eq(world.state.binds.RD.get("C2.0", 0), 0)), "Space successor does not inherit planetary binding")
|
||||||
|
dispose(world)
|
||||||
|
var capacity = load("res://world/owniverse/civilization_state.gd")
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 10
|
||||||
|
world.state.amount["C2.0"] = 8
|
||||||
|
var budget: Dictionary = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||||
|
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 2), "Growth consumes only remaining stellar capacity")
|
||||||
|
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 0), "A reservation cannot be spent twice")
|
||||||
|
world.state.amount["C2.0"] = 1
|
||||||
|
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||||
|
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1), 1), "A transition reuses the source reservation")
|
||||||
|
check(E.eq(budget.free.DwStar, 0), "C2.1 reserves ten dwarf stars")
|
||||||
|
world.state.amount["C2.0"] = 0
|
||||||
|
world.state.binds.SC = {"RD": 10}
|
||||||
|
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||||
|
check(E.eq(budget.capacity.DwStar, 20), "Stars inside structures are counted once")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 12
|
||||||
|
world.state.amount["C2.1"] = 2
|
||||||
|
world.state.amount["C2.0"] = 5
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check((E.eq(world.state.amount["C2.1"], 1) and E.eq(world.state.amount["C2.0"], 2)), "Shared budget supports advanced civilizations first")
|
||||||
|
check(E.eq(world.state.binds.RD["C1.9"], 4), "Excess cascades to supported planetary stage")
|
||||||
|
var snapshot: Dictionary = world.state.duplicate(true)
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state, snapshot), "Capacity enforcement is idempotent")
|
||||||
|
world.state.amount.RD = 0
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check((E.eq(world.state.amount["C2.1"], 0) and E.eq(world.state.amount["C2.0"], 0)), "Vanished stars force all unsupported space civilizations down")
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 7), "Forced degradation conserves civilizations")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.LGW = 20
|
||||||
|
world.state.amount.SMBH = E.of("2.0e22")
|
||||||
|
world.state.amount["C4.2"] = 3
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
|
||||||
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 2
|
||||||
|
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||||
|
row.progress = "1"
|
||||||
|
row.degradation = "0"
|
||||||
|
row.colonization = "0"
|
||||||
|
world.state.binds.RD = {"C1.9": 5}
|
||||||
|
world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}]
|
||||||
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
|
check((E.eq(world.state.amount["C2.0"], 2) and E.eq(world.state.binds.RD["C1.9"], 3)), "Unfunded transition retains the original planetary civilization")
|
||||||
|
dispose(world)
|
||||||
|
print(("Civilization state: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://nvgiilyqitw
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
var description = load("res://world/owniverse/item_description.gd")
|
||||||
|
var world = fixture()
|
||||||
|
world.state.amount.H = 8
|
||||||
|
world.state.amount.He = 2
|
||||||
|
for locale in ["en", "ru"]:
|
||||||
|
global.Locale = locale
|
||||||
|
var text: String = description.render(world, global, "RD")
|
||||||
|
check((text.contains("[b]") and (not text.contains("%value"))), E.add("Localized template resolved: ", locale))
|
||||||
|
check(text.contains("#40cc40"), E.add("Affordable cost green: ", locale))
|
||||||
|
check(text.contains("40 000 000 000"), E.add("Lifespan substituted: ", locale))
|
||||||
|
text = description.render(world, global, "He")
|
||||||
|
check((text.contains("He: 20.00% / H: 80.00%") and (not text.contains("%value"))), E.add("Helium metadata and mixture: ", locale))
|
||||||
|
text = description.render(world, global, "Shovelin", "Force")
|
||||||
|
check(E.eq(text.count("]—[/color]"), 4), E.add("Missing force levels use dash: ", locale))
|
||||||
|
text = description.render(world, global, "YD")
|
||||||
|
check(text.contains("#ff6060"), E.add("Insufficient cost red: ", locale))
|
||||||
|
var reordered: Dictionary = {"text": "%value0% / %value1%", "value0": "lifespan", "value1": "amount"}
|
||||||
|
check(E.eq(global.render_template(reordered, {"amount": "12", "lifespan": "99"}), "99 / 12"), "Metadata chooses variables, not fixed argument positions")
|
||||||
|
world.state.amount.RD = 2
|
||||||
|
world.state.binds.G = {"RD": 3, "HP": 7}
|
||||||
|
var values: Dictionary = description.variables(world, global, "RD", E.neg(2))
|
||||||
|
check(((values.amount.contains("2") and values.amount.contains("(5)")) and values.amount.contains("#888888")), "Free and total counts with gray binding total")
|
||||||
|
check((values.amount.contains("#ff6060") and values.amount.contains("−2.0")), "Real-time decrease red")
|
||||||
|
values = description.variables(world, global, "HP")
|
||||||
|
check(E.eq(values.amount, "7"), "Planet description includes bound planets")
|
||||||
|
world.state.amount.EON1 = 2
|
||||||
|
world.state.binds.G.EON2 = 3
|
||||||
|
var saved_state: Dictionary = world.state.duplicate(true)
|
||||||
|
var counters: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||||
|
check(((E.eq(counters.HP, 7) and E.eq(counters.LP, 5)) and E.eq(counters.Planet, 12)), "Planet counters include bound objects and aggregate eons")
|
||||||
|
check(E.eq(counters.RD, 2), "Star counters retain available quantity")
|
||||||
|
check(E.eq(world.state, saved_state), "Reading counters does not mutate simulation")
|
||||||
|
check(E.eq(world.presentation.amounts(world.state, world.catalog, true).HP, 0), "Produced counters remain independent of current population")
|
||||||
|
values = description.variables(world, global, "RD", 2)
|
||||||
|
check((values.amount.contains("#40cc40") and values.amount.contains("+2.0")), "Increase green")
|
||||||
|
var stage_script = load("res://stage/stage.gd")
|
||||||
|
check(stage_script.can_instantiate(), "Stage integration compiles")
|
||||||
|
var stage = stage_script.new()
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.owniverse = world
|
||||||
|
var label: RichTextLabel = RichTextLabel.new()
|
||||||
|
stage.g_item_descriptions = [label]
|
||||||
|
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
|
||||||
|
stage._refresh_descriptions()
|
||||||
|
check((label.bbcode_enabled and label.text.contains("Красный карлик")), "Stage displays localized rich text")
|
||||||
|
stage._update_descriptions(0.25)
|
||||||
|
world.state.amount.RD = E.add(world.state.amount.RD, 1)
|
||||||
|
stage._update_descriptions(0.25)
|
||||||
|
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
|
||||||
|
global.Locale = "en"
|
||||||
|
stage._update_descriptions(0.25)
|
||||||
|
check(label.text.contains("Red Dwarf"), "Selected description follows locale changes")
|
||||||
|
label.free()
|
||||||
|
stage.free()
|
||||||
|
dispose(world)
|
||||||
|
print(("Descriptions: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1efrwyimgle7
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const B = preload("res://world/owniverse/math/big_integer.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func verify(condition: bool, label: String):
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures += 1
|
||||||
|
push_error(label)
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
rng.seed = 619
|
||||||
|
for trial in 300:
|
||||||
|
var n: int = rng.randi_range(0, 100)
|
||||||
|
var m: int = rng.randi_range(1, 100)
|
||||||
|
var a: int = rng.randi_range(0, 1000)
|
||||||
|
var b: int = rng.randi_range(0, 1000)
|
||||||
|
var expected: int = 0
|
||||||
|
for i in n: expected += (a * i + b) / m
|
||||||
|
verify(E.eq(E.floor_sum(n, a, b, m), expected), "floor sum matches explicit windows")
|
||||||
|
var huge = E.of("1e40")
|
||||||
|
verify(E.lt(E.of("1e1000"), INF), "finite huge number is below positive infinity")
|
||||||
|
verify(E.gt(E.of("-1e1000"), -INF), "finite huge negative number is above negative infinity")
|
||||||
|
verify(E.gt(INF, E.of("1e1000")), "positive infinity is above finite huge number")
|
||||||
|
verify(not E.eq(E.of("1e1000"), INF), "huge number is not infinity")
|
||||||
|
verify(E.eq(E.floor_sum(huge, 2, 1, 2), E.divide(E.multiply(huge, E.subtract(huge, 1)), 2)), "floor sum supports huge window counts")
|
||||||
|
var rows = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/big_integer_oracle.json"))
|
||||||
|
for row in rows:
|
||||||
|
var a: Array = B.parse(row[0])
|
||||||
|
var b: Array = B.parse(row[1])
|
||||||
|
var qr: Array = B.divmod(a, b)
|
||||||
|
verify(B.decimal(B.add(a, b)) == row[2], "independent big sum")
|
||||||
|
verify(B.decimal(B.multiply(a, b)) == row[3], "independent big product")
|
||||||
|
verify(B.decimal(qr[0]) == row[4], "independent big quotient")
|
||||||
|
verify(B.decimal(qr[1]) == row[5], "independent big remainder")
|
||||||
|
verify(E.fixed(E.multiply(E.of(row[0]), E.of(row[1]))) == row[3], "dynamic product")
|
||||||
|
verify(E.eq(E.mul_div(E.of(row[0]), E.of(row[1]), E.of(row[1])), E.of(row[0])), "fused conservation")
|
||||||
|
for text in ["9223372036854775807", "-9223372036854775808", "9223372036854775808", "-9223372036854775809", "1e300", "1e1000"]:
|
||||||
|
var n = E.of(text)
|
||||||
|
verify(E.eq(E.subtract(E.add(n, 1), n), 1), "unit preserved at " + text)
|
||||||
|
verify(E.eq(E.decode(JSON.parse_string(JSON.stringify(E.encode(n)))), n), "round trip " + text)
|
||||||
|
for text in ["1.25", "-1.25", "123.5"]:
|
||||||
|
verify(E.approx(E.of(text)) == float(text), "approx fraction " + text)
|
||||||
|
verify(not E.valid_encoded({"$exact": [[], "0000000000000000", 1]}), "reject empty blocks")
|
||||||
|
verify(not E.valid_encoded({"$exact": [[1, 0], "0000000000000000", 1]}), "reject noncanonical blocks")
|
||||||
|
verify(E.gt(E.power(E.of("1e400"), -1), 0) == false, "float fraction underflows beyond its range")
|
||||||
|
verify(absf(E.approx(E.logarithm(E.of("1e1000"))) - 1000 * log(10.0)) < 1e-8, "logarithm beyond float range")
|
||||||
|
verify(E.eq(E.square_root(E.of("1e1000")), E.of("1e500")), "square root beyond float range")
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
var group: String = world.catalog.entities[id].group_as
|
||||||
|
if group != "":
|
||||||
|
world.state.amount[group] = 0
|
||||||
|
world.state.stats.total_amount[group] = 0
|
||||||
|
world.state.amount.H = E.of("1e1000")
|
||||||
|
world.state.amount.S = E.of("1e1000")
|
||||||
|
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||||
|
verify(E.finite(environment.density) and E.gt(environment.density, 0), "huge density remains finite")
|
||||||
|
verify(E.near(environment.blast, 1), "huge geometric ratio stays valid")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
print("Dynamic math: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://8vkudj0tlnng
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Q = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(actual, expected: String, label: String):
|
||||||
|
checks += 1
|
||||||
|
if str(actual) != expected:
|
||||||
|
failures += 1
|
||||||
|
push_error(label + ": " + str(actual) + " != " + expected)
|
||||||
|
|
||||||
|
func check_number(actual, expected: String, label: String):
|
||||||
|
var reference = E.of(expected)
|
||||||
|
checks += 1
|
||||||
|
if E.gt(E.absolute(E.subtract(actual, reference)), E.of("1e-12")):
|
||||||
|
failures += 1
|
||||||
|
push_error(label + ": " + str(actual) + " != " + expected)
|
||||||
|
|
||||||
|
func run():
|
||||||
|
# Expected strings were generated independently with Python fractions.Fraction.
|
||||||
|
var cases = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/exact_oracle.json"))
|
||||||
|
for row in cases:
|
||||||
|
var a = E.of(row[0])
|
||||||
|
var b = E.of(row[1])
|
||||||
|
check_number(E.add(a, b), row[2], "sum")
|
||||||
|
check_number(E.subtract(a, b), row[3], "difference")
|
||||||
|
check_number(E.multiply(a, b), row[4], "product")
|
||||||
|
check_number(E.divide(a, b), row[5], "quotient")
|
||||||
|
check(E.floor_value(E.divide(a, b)), row[6], "floor")
|
||||||
|
for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]:
|
||||||
|
var value = E.of(text)
|
||||||
|
check(E.eq(E.decode(JSON.parse_string(JSON.stringify(E.encode(value)))), value), "true", "JSON round trip")
|
||||||
|
check(E.add(E.of("9223372036854775807"), 1), "9223372036854775808", "int64 overflow promoted")
|
||||||
|
check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted")
|
||||||
|
check(Q.production_step([E.of("1e27")]), "1", "large integral recipe")
|
||||||
|
check(Q.production_step([E.of("1e-13")]), "10000000000000", "tiny fractional recipe")
|
||||||
|
var count = E.of("1000000000000000000000000001")
|
||||||
|
var allocation = Q.allocate_whole({"a": count, "b": count, "c": count}, count)
|
||||||
|
check(Q.sum(allocation), str(count), "allocation conserves every unit")
|
||||||
|
check(allocation.a, "333333333333333333333333334", "deterministic remainder")
|
||||||
|
var clock = Clock.advance({"datetime": E.of("1e27")}, 0.000000001, 1)
|
||||||
|
check_number(E.subtract(clock.datetime, E.of("1e27")), "1/1000000000", "single tick at huge age")
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
var group: String = world.catalog.entities[id].group_as
|
||||||
|
if group != "":
|
||||||
|
world.state.amount[group] = 0
|
||||||
|
world.state.stats.total_amount[group] = 0
|
||||||
|
world.production.produce_hs("H", 1)
|
||||||
|
check(world.state.energy, "999999999999999999999999999", "spend one energy")
|
||||||
|
var life = world.life.life_products(world.state, world.catalog.entities.EON5, count)
|
||||||
|
check(Q.sum(life), str(count), "life conservation at 1e27")
|
||||||
|
check(E.bounded(count), str(count), "integer counts never quantized")
|
||||||
|
check(E.mul_div(E.of("1e27"), 15, 100), "150000000000000000000000000", "exact percentage")
|
||||||
|
check(E.mul_div(count, count, count), str(count), "fused product beyond storage range")
|
||||||
|
var limit = E.of("1e27")
|
||||||
|
check(E.floor_value(E.mul_div(E.subtract(limit, 1), E.subtract(limit, 3), E.subtract(limit, 2))), "999999999999999999999999997", "fused ratio immediately below whole boundary")
|
||||||
|
check(E.floor_value(E.divide(E.MIN_INT, -3)), "3074457345618258602", "MIN_INT division preserves quotient")
|
||||||
|
check(E.floor_value(E.divide(-10, 3)), "-4", "native negative fractional division")
|
||||||
|
check(E.mul_div(E.MAX_INT, 0, 7), "0", "native zero fused product")
|
||||||
|
check(E.subtract(E.add(count, 1), count), "1", "single unit at E27")
|
||||||
|
check(E.floor_value(E.divide(E.subtract(count, 1), count)), "0", "division must not round up a whole object")
|
||||||
|
check(E.fixed(E.add(count, E.of("0.125")), 3), "1000000000000000000000000001.125", "formatting without oversized intermediate")
|
||||||
|
check(E.scientific(E.of("9.999999e27"), 2), "1.00E+28", "display rounding at upper boundary")
|
||||||
|
check(absf(E.approx(E.power(10, -100)) / E.approx(E.of("1e-100")) - 1.0) < 1e-15, "true", "negative power retains float precision without huge intermediate")
|
||||||
|
var largest = E.of("9999999999999999999999999999")
|
||||||
|
check(E.eq(E.decode(E.encode(largest)), largest), "true", "upper range round trip")
|
||||||
|
check(E.valid_encoded(E.encode(largest)), "true", "upper range encoding valid")
|
||||||
|
for tiny in ["1e-13", "1e-20", "1e-100", "1e-310"]:
|
||||||
|
var value = E.of(tiny)
|
||||||
|
check(E.gt(value, 0), "true", "tiny fraction remains nonzero")
|
||||||
|
check(E.eq(value, E.decode(JSON.parse_string(JSON.stringify(E.encode(value))))), "true", "tiny float round trip")
|
||||||
|
for invalid in [{"$fixed": ["0", "0", "ffffffffffffffff", 1]}, {"$fixed": ["0", "-1", "0000000000000000", 1]}, {"$exact": "1/3"}, {"$exact": "10000000000000000000000000000"}, 1e100]:
|
||||||
|
check(E.valid_encoded(invalid), "false", "reject invalid saved number")
|
||||||
|
world.state.amount.S = E.of("1e24")
|
||||||
|
world.state.amount.H = E.of("1e24")
|
||||||
|
world.state.amount.RD = 1234567
|
||||||
|
world.state.amount.YD = 765432
|
||||||
|
world.state.amount.STN = 654321
|
||||||
|
world.speed = 6
|
||||||
|
var started: int = Time.get_ticks_msec()
|
||||||
|
for i in 100:
|
||||||
|
world.state.amount.RD = E.add(world.state.amount.RD, i + 1)
|
||||||
|
world.evolute(0.1)
|
||||||
|
check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded")
|
||||||
|
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
|
||||||
|
var saves = root.get_node("SaveManager")
|
||||||
|
check(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
print("Exact math: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(0 if failures == 0 else 1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://wxhre4pgx1lb
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
func check(condition: bool, message: String):
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
Fixture.seed(world, root.get_node("Global"))
|
||||||
|
return world
|
||||||
|
func dispose(world):
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run():
|
||||||
|
Engine.max_fps = 0
|
||||||
|
var direct = fixture()
|
||||||
|
var sliced = fixture()
|
||||||
|
for delta in [1.0 / 60.0, 0.133333333333]:
|
||||||
|
direct.evolute(delta)
|
||||||
|
var clock: Dictionary = Clock.advance(sliced.state.time, delta, sliced.speed)
|
||||||
|
var budget = load("res://world/owniverse/frame_budget.gd").new(self, 1)
|
||||||
|
await sliced.evolution.evolute_to_datetime(clock.datetime, clock, budget)
|
||||||
|
check(E.eq(direct.state, sliced.state), "Sliced transaction exactly matches uninterrupted evolution")
|
||||||
|
dispose(direct)
|
||||||
|
dispose(sliced)
|
||||||
|
direct = fixture()
|
||||||
|
sliced = fixture()
|
||||||
|
var initial_h: Variant = sliced.state.amount.H
|
||||||
|
sliced.evolute_frame(1.0 / 60.0, self)
|
||||||
|
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
||||||
|
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
|
||||||
|
sliced.request_production("H", 1, true)
|
||||||
|
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
|
||||||
|
sliced.evolute_frame(0.01, self)
|
||||||
|
var requested: Dictionary = sliced.frame_clock.duplicate()
|
||||||
|
sliced.speed = 3
|
||||||
|
check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction")
|
||||||
|
while sliced.frame_busy: await process_frame
|
||||||
|
direct.evolute(1.0 / 60.0)
|
||||||
|
direct.production.produce_hs("H", 1)
|
||||||
|
direct.speed = 3
|
||||||
|
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
|
||||||
|
sliced.evolute_frame(0.001, self)
|
||||||
|
var second_clock: Dictionary = Clock.advance(requested, 0.001, 3)
|
||||||
|
var active: Dictionary = sliced.frame_active_clock.duplicate()
|
||||||
|
while sliced.frame_busy: await process_frame
|
||||||
|
direct.evolution.evolute_to_datetime(active.datetime, active)
|
||||||
|
check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock")
|
||||||
|
check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock")
|
||||||
|
check(sliced.frame_commits == 2, "Only complete simulation states are committed")
|
||||||
|
dispose(direct)
|
||||||
|
dispose(sliced)
|
||||||
|
var original = fixture()
|
||||||
|
original.evolute_frame(1.0 / 60.0, self)
|
||||||
|
original.request_production("H", 123, true)
|
||||||
|
original.evolute_frame(0.01, self)
|
||||||
|
original.speed = 3
|
||||||
|
var checkpoint: Dictionary = E.decode(E.encode(original.frame_checkpoint()))
|
||||||
|
var published: Dictionary = E.decode(E.encode(original.state))
|
||||||
|
var expected_clock: Dictionary = original.frame_active_clock.duplicate()
|
||||||
|
dispose(original)
|
||||||
|
for frame in 3: await process_frame
|
||||||
|
var resumed = fixture()
|
||||||
|
var expected = fixture()
|
||||||
|
resumed.state = published.duplicate(true)
|
||||||
|
expected.state = published.duplicate(true)
|
||||||
|
resumed.restore_frame_checkpoint(checkpoint)
|
||||||
|
resumed.evolute_frame(0.0, self)
|
||||||
|
while resumed.frame_busy: await process_frame
|
||||||
|
expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock)
|
||||||
|
expected.production.produce_hs("H", 123)
|
||||||
|
expected.speed = 3
|
||||||
|
check(E.eq(expected.state, resumed.state), "Checkpoint recomputes interrupted transaction and preserves queued production")
|
||||||
|
check(E.eq(resumed.frame_clock, checkpoint.requested_clock), "Checkpoint preserves all requested elapsed time")
|
||||||
|
dispose(resumed)
|
||||||
|
dispose(expected)
|
||||||
|
var cancelled = fixture()
|
||||||
|
cancelled.evolute_frame(0.133333333333, self)
|
||||||
|
check(cancelled.frame_busy, "Cancellation fixture has suspended work")
|
||||||
|
var budget_ref: RefCounted = cancelled.frame_budget
|
||||||
|
dispose(cancelled)
|
||||||
|
for frame in 3: await process_frame
|
||||||
|
check(budget_ref.cancelled, "Resetting the world cancels suspended work without publishing")
|
||||||
|
var invalid = fixture()
|
||||||
|
var before: Dictionary = invalid.state.duplicate(true)
|
||||||
|
invalid.evolute_frame(1e308, self)
|
||||||
|
invalid.evolute(1e308)
|
||||||
|
check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API")
|
||||||
|
dispose(invalid)
|
||||||
|
print("Frame evolution: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://te15r63sqhxs
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
var expected: Array = []
|
||||||
|
for key in world.catalog.entities:
|
||||||
|
if E.eq(world.catalog.entities[key].group_as, id):
|
||||||
|
expected.append(key)
|
||||||
|
check(E.eq(world.catalog.is_group(id), ((not expected.is_empty()))), E.add("Group classification: ", id))
|
||||||
|
check(E.eq(world.catalog.members(id), ((expected if (not expected.is_empty()) else [id]))), E.add("Group members: ", id))
|
||||||
|
var members: Array = world.catalog.members("Star")
|
||||||
|
members.clear()
|
||||||
|
check((not world.catalog.members("Star").is_empty()), "Returned members are independent")
|
||||||
|
world.catalog.entities.N.group_as = "TestGroup"
|
||||||
|
world.catalog.rebuild_group_index()
|
||||||
|
check(E.eq(world.catalog.members("TestGroup"), ["N"]), "Explicit definition reload")
|
||||||
|
world.catalog.entities.N.free()
|
||||||
|
world.catalog.entities.erase("N")
|
||||||
|
check((not world.catalog.is_group("TestGroup")), "Removed definition invalidates index")
|
||||||
|
print(("Group index: %d checks, %d failures" % [checks, failures]))
|
||||||
|
dispose(world)
|
||||||
|
quit((1 if failures else 0))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bykrkembtacbw
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
world.state.amount.N = 0
|
||||||
|
world.state.amount.HP = 40000
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state.batches.HP.size(), 1), "Old HP receives life cohort")
|
||||||
|
check(E.eq(world.state.batches.HP[0].start_year, 5000000), "HP uses source span")
|
||||||
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state, before), "Initialization is idempotent")
|
||||||
|
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, 40)))
|
||||||
|
check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability")
|
||||||
|
check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP")
|
||||||
|
check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear")
|
||||||
|
check(E.eq(world.state.stats.total_amount.HP, 0), "Retry does not inflate creation statistics")
|
||||||
|
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets")
|
||||||
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||||
|
var restored = fixture()
|
||||||
|
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
|
world.evolution.evolve_batches(world.state, 11000000)
|
||||||
|
restored.evolution.evolve_batches(restored.state, 11000000)
|
||||||
|
check(E.eq(world.state, restored.state), "Life continuation preserves cohorts and carries")
|
||||||
|
dispose(restored)
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 1
|
||||||
|
world.state.binds.RD = {"EON5": 40000}
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40)))
|
||||||
|
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
|
||||||
|
check(E.eq(world.state.amount["C0.0"], 0), "Life transition keeps planet bound")
|
||||||
|
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
var progress: Variant = 0
|
||||||
|
var degraded: Variant = 0
|
||||||
|
var valid_outcomes: bool = true
|
||||||
|
for i in 4001:
|
||||||
|
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1)
|
||||||
|
var total: Variant = 0
|
||||||
|
for value in products.values():
|
||||||
|
total = E.add(total, value)
|
||||||
|
valid_outcomes = ((valid_outcomes and E.ge(value, 0)) and E.eq(value, E.floor_value(value)))
|
||||||
|
valid_outcomes = (valid_outcomes and E.eq(total, 1))
|
||||||
|
progress = E.add(progress, products.get("EON1", 0))
|
||||||
|
degraded = E.add(degraded, products.get("RP", 0))
|
||||||
|
check(valid_outcomes, "Every single-planet attempt has whole outcomes and conserves population")
|
||||||
|
check((E.eq(progress, 1) and E.eq(degraded, 10)), "Rare outcomes accumulate deterministically")
|
||||||
|
dispose(world)
|
||||||
|
|
||||||
|
world = fixture()
|
||||||
|
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
|
||||||
|
world.state.binds.RD = {"HP": 2}
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state.batches.HP[0].created_from, E.of("123.4")), "Old bound worlds begin life at load time")
|
||||||
|
world.stellar.destroy_planets(world.state, "RD", 1, 1)
|
||||||
|
world.evolution.evolve_batches(world.state, 10000000)
|
||||||
|
check((E.eq(world.ledger.physical_amounts(world.state).HP, 0) and E.eq(world.state.binds.RD.RP, 2)), "Destroyed life does not resurrect from cohort")
|
||||||
|
dispose(world)
|
||||||
|
world = fixture()
|
||||||
|
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
|
||||||
|
world.state.amount["C0.0"] = 40000
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state.batches["C0.0"][0].created_from, E.of("123.4")), "Civilization cohorts begin at the current time")
|
||||||
|
before = world.state.duplicate(true)
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
check(E.eq(world.state, before), "Life batch initialization is idempotent")
|
||||||
|
world.evolution.evolve_batches(world.state, E.of("2173.4"))
|
||||||
|
check((E.eq(world.state.amount["C0.1"], 700) and E.eq(world.state.amount.EON5, 1000)), "C0.0 progresses and degrades by Unity rates")
|
||||||
|
check(E.eq(world.state.amount["C0.0"], 38300), "C0.0 remainder stays alive")
|
||||||
|
dispose(world)
|
||||||
|
world = fixture()
|
||||||
|
world.state.amount.RD = 1
|
||||||
|
world.state.binds.RD = {"C0.1": 40000}
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
world.evolution.evolve_batches(world.state, 2050)
|
||||||
|
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
|
||||||
|
check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity")
|
||||||
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||||
|
dispose(world)
|
||||||
|
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dghmnvnl7ks1b
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(value: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not value):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var scene = load("res://world/world.tscn").instantiate()
|
||||||
|
var stage = scene.get_node("Stage")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.set_script(load("res://tests/fixtures/ui_stage.gd"))
|
||||||
|
root.add_child(scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var world = stage.owniverse
|
||||||
|
world.state.binds.RD = {"EON1": 10, "EON2": 4, "C0.0": 3}
|
||||||
|
world.state.amount.EON1 = 2
|
||||||
|
world.state.stats.total_amount.EON1 = 12
|
||||||
|
world.state.stats.total_amount.EON2 = 4
|
||||||
|
world.state.stats.total_amount["C0.0"] = 3
|
||||||
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
var counts: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||||
|
check(((E.eq(counts.EON1, 12) and E.eq(counts.EON2, 4)) and E.eq(counts.LP, 16)), "Individual eons and their total include bound planets")
|
||||||
|
check(((E.eq(counts["C0.0"], 3) and E.eq(counts.C0, 3)) and E.eq(counts.Planet, 19)), "Civilization detail does not double the planetary total")
|
||||||
|
check((E.eq(world.presentation.amounts(world.state, world.catalog, true).EON1, 12) and E.eq(world.presentation.amounts(world.state, world.catalog, true).LP, 16)), "Historical counts retain individual and aggregate values")
|
||||||
|
check(E.eq(world.state, before), "Presentation does not modify simulation state")
|
||||||
|
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
|
||||||
|
var pending_before: Array = pending.duplicate()
|
||||||
|
var decision: Dictionary = world.presentation.unlocks(world.state, world.catalog, pending)
|
||||||
|
check(E.eq(decision.remaining, ["EON3"]), "Only unmet requirements remain pending")
|
||||||
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
|
check(decision.visible.has(id), E.add("Unlock projection includes detail and alias: ", id))
|
||||||
|
check((E.eq(pending, pending_before) and E.eq(world.state, before)), "Unlock evaluation leaves inputs untouched")
|
||||||
|
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
|
||||||
|
world.presentation.deferred_updates = true
|
||||||
|
world.state.amount.EON1 = E.add(world.state.amount.EON1, 1)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
check((world.presentation.dirty and E.eq(world.presentation.pending_amounts, snapshot)), "Deferred refresh retains pending snapshot")
|
||||||
|
world.presentation.refresh(world.state, world.catalog, true)
|
||||||
|
check(((not world.presentation.dirty) and E.eq(world.presentation.pending_amounts.EON1, 13)), "Forced refresh publishes latest amounts")
|
||||||
|
world.state.amount.EON1 = E.subtract(world.state.amount.EON1, 1)
|
||||||
|
world.presentation.deferred_updates = false
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
|
check(stage.owniverse_items[id].visible, E.add("Unlock both levels: ", id))
|
||||||
|
check(E.ne(stage.owniverse_items[id].description.text, ""), E.add("Populate the icon counter: ", id))
|
||||||
|
check((not stage.owniverse_items.EON3.visible), "Unreached eon remains locked")
|
||||||
|
stage.item_activated(stage.owniverse_items.LP)
|
||||||
|
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
|
||||||
|
stage.item_activated(stage.owniverse_items.C0)
|
||||||
|
check((stage.owniverse_items["C0.0"].is_visible_in_tree() and (not stage.owniverse_items.EON1.is_visible_in_tree())), "Selecting C0 switches the detail panel")
|
||||||
|
world.state.binds.RD.EON2 = 0
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
check((stage.owniverse_items.EON2.visible and E.eq(stage.owniverse_items.EON2.description.text, "")), "Previously reached stage stays unlocked after extinction with no stale count")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
scene.free()
|
||||||
|
print(("Life icons: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c4p1ad0ij32xo
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var World = load("res://world/owniverse/owniverse.gd")
|
||||||
|
var first = World.new()
|
||||||
|
var second = World.new()
|
||||||
|
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
|
||||||
|
var references: Array = []
|
||||||
|
for name in names:
|
||||||
|
check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name))
|
||||||
|
check(E.eq(first.get(name).world, first), E.add("Module refers to its owner: ", name))
|
||||||
|
references.append(weakref(first.get(name)))
|
||||||
|
first.state.amount.H = 123
|
||||||
|
first.presentation.pending_amounts.H = 123
|
||||||
|
check(((not second.state.amount.has("H")) and second.presentation.pending_amounts.is_empty()), "State and pending presentation are isolated")
|
||||||
|
first.free()
|
||||||
|
for reference in references:
|
||||||
|
check(E.eq(reference.get_ref(), null), "Module is released with its world")
|
||||||
|
check(E.eq(second.evolution.world, second), "Other world remains usable")
|
||||||
|
var snapshot: Dictionary = second.state.duplicate(true)
|
||||||
|
second.evolute(NAN)
|
||||||
|
second.evolute(E.neg(1))
|
||||||
|
check(E.eq(second.state, snapshot), "Entry point rejects invalid time deltas")
|
||||||
|
second.free()
|
||||||
|
print(("Module lifecycle: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((1 if failures else 0))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d2xk4ldldsgo3
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
var global: Node = root.get_node("Global")
|
||||||
|
for id in global.OwniverseEntities:
|
||||||
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
|
world.catalog.entities[id] = entity
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
if E.ne(entity.group_as, ""):
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
world.catalog.entities["N"].life_product = {}
|
||||||
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
|
world.catalog.entities["N"].auto = 0
|
||||||
|
world.state.amount.N = 1
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values():
|
||||||
|
entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var world = fixture()
|
||||||
|
world.evolution.initialize_life_batches()
|
||||||
|
world.catalog.entities.N.cost = {"H": 2}
|
||||||
|
world.catalog.entities.N.bind = {"S": 3}
|
||||||
|
world.state.amount.H = 10
|
||||||
|
world.state.amount.S = 0
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), 0), "Default maximum respects missing bindings")
|
||||||
|
world.state.amount.S = 30
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), 5), "Default maximum uses recipe resources without an oversized sentinel")
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N, 2), 2), "Explicit production limit is preserved")
|
||||||
|
world.state.amount.H = E.of("1e27")
|
||||||
|
world.state.amount.S = E.of("1e27")
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), E.floor_value(E.divide(E.of("1e27"), 3))), "Default maximum at E27 retains integer precision")
|
||||||
|
world.state.amount.BH25 = 4
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.SMBH), world.production.smbh_available()), "Default SMBH maximum uses available black-hole mass")
|
||||||
|
world.state.amount.BH25 = 0
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.SMBH), 0), "Empty SMBH maximum is zero")
|
||||||
|
world.state.amount.H = 10
|
||||||
|
world.state.amount.S = 0
|
||||||
|
world.state.auto_resources = {"H": 10}
|
||||||
|
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
|
||||||
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
|
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true)), "Binding failure rejects production")
|
||||||
|
check(E.eq(world.state, before), "Failure preserves costs, batches and budget")
|
||||||
|
world.state.amount.S = 10
|
||||||
|
var old_batches: Array = world.state.batches.H
|
||||||
|
check(world.production.try_production(world.state, world.catalog.entities.N, 2, true), "Funded production succeeds")
|
||||||
|
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
|
||||||
|
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
|
||||||
|
check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated")
|
||||||
|
print(("Transaction: %d checks, %d failures" % [checks, failures]))
|
||||||
|
dispose(world)
|
||||||
|
quit((1 if failures else 0))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://byjixspismsnh
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user