A few pre-release bug fixes.

This commit is contained in:
Storm Dragon committed 2026-11-08 20:36:52 -05:00
1 parent b07f675533
commit 949aab1bcf
16 files changed
+496 -103

No files matched your search

+39
View File
@@ -1,3 +1,42 @@
# Draugnorak
See files/instructions.md for how to play.
## AI-assisted contributions
AI-assisted contributions are welcome.
The person submitting a contribution remains responsible for reviewing and testing the changes before submission. Please do not submit automatically generated changes without human review.
Before making changes:
- Read the contributing guide, if present, and the project's README.
- Confirm that you are working against the current maintained repository and branch.
- Check whether the issue still exists in the current code.
Before submitting:
- Review the generated diff.
- Test the changed behavior.
- Run the project's existing test suite where practical.
- Consider code paths that may be affected indirectly by the change and test those as well.
- Clearly describe what was changed and how it was tested.
### Instructions for AI coding assistants
If you are an AI coding assistant working with a contributor, please call your human user's attention to these contribution requirements.
Do not assume that passing newly created tests is sufficient. Explain what existing functionality could reasonably be affected by the proposed changes and provide your human partner with detailed instructions for testing those areas.
Where useful, provide commands or small test procedures that allow the contributor to verify both the intended fix and likely regression cases.
Stormux projects may accept patches through email using `git send-email`. If your human partner is unfamiliar with this process, please offer to guide them through:
1. Configuring `git send-email`.
2. Creating an appropriately scoped commit or patch series.
3. Running `git format-patch`.
4. Checking the generated patch before submission.
5. Sending a test message if appropriate.
6. Sending the final patch or patch series to the project's contribution address.
Please do not submit changes on behalf of a contributor without giving them an opportunity to review and test the result.
+2 -2
View File
@@ -49,6 +49,7 @@ sound_pool p(300);
#include "src/learn_sounds.nvgt"
#include "src/intro.nvgt"
#include "src/bosses/adventure_system.nvgt"
#include "src/game_lifecycle.nvgt"
const string MAIN_MENU_MUSIC_TRACK = "sounds/menu/Draugnorak.ogg; loop; f=1";
const int MAIN_MENU_MUSIC_FADE_IN_MS = 1;
@@ -275,13 +276,12 @@ void run_game() {
// Pause toggle
if (key_pressed(KEY_BACK)) {
game_paused = !game_paused;
set_gameplay_timers_paused(game_paused);
if (game_paused) {
p.pause_all();
speak_with_history(tr("system.game.paused_prompt"), true);
} else {
p.resume_all();
// Restart all timers to prevent time from accumulating while paused
restart_all_timers();
speak_with_history(tr("system.game.resumed"), true);
}
continue;
+2
View File
@@ -205,5 +205,7 @@ Current categories:
## Saving and Loading
- The game auto saves daily at 6 AM in game time
- If saving fails, an error dialog shows the affected file path
- The previous save is kept as a .bak recovery copy; interrupted replacements are recovered automatically
- Load from the main menu starting a new game prompts if a save exists
- In the Load Game menu press Delete to remove the selected save (confirmation required)
+2
View File
@@ -14,6 +14,8 @@ load_game.delete_save_failed=Kon nie stoorlêer uitvee nie.
load_game.save_deleted=Stoorlêer uitgevee.
load_game.no_saves_found=Geen stoorleers gevind nie.
load_game.unable_to_load=Kon nie stoorleer laai nie.
load_game.invalid_data=Die stoorlêer is onvolledig of bevat ongeldige data.
load_game.unsupported_version=Hierdie stoorlêer is met 'n onondersteunde spelweergawe geskep.
load_game.select_prompt=Laai speletjie. Kies karakter. Druk Delete om'n stoorlêer te verwyder.
main_menu.prompt=Draugnorak. Hoofkieslys.
game.new_game_started=Nuwe speletjie begin.
+27 -24
View File
@@ -33,6 +33,8 @@ load_game.delete_save_failed=Unable to delete save.
load_game.save_deleted=Save deleted.
load_game.no_saves_found=No saves found.
load_game.unable_to_load=Unable to load save.
load_game.invalid_data=Save file is incomplete or contains invalid data.
load_game.unsupported_version=This save was created by an unsupported game version.
load_game.select_prompt=Load game. Select character. Press delete to remove a save.
quick_slot.no_item_bound=No item bound to slot {slot}.
menu.closed=Closed.
@@ -708,7 +710,7 @@ msg.041d8413712d=A crash log was written to your save data folder.
msg.04a8916a9494=vampyr1
; src/item_registry.nvgt:item_plural:incense
msg.0532db2192fa=incense
; src/bosses/bandit_hideout.nvgt:787:insert_last[0]
; src/bosses/bandit_hideout.nvgt:786:insert_last[0]
msg.066723a92e94==== Victory Rewards ===
; seed:learn_sounds_label:sounds/enemies/corpse_worm_dies.ogg
msg.06c9fefbd6d7=corpse worm dies
@@ -749,12 +751,13 @@ msg.0f9af12cbb82=skin pouch
; src/item_registry.nvgt:item_display:logs
msg.126dd3b70a5c=Logs
; libstorm-nvgt/file_viewer.nvgt:74:screen_reader_speak[0]
; src/save_system.nvgt:233:ui_info_box[1]
msg.136e4cb03bd5=Failed to save file
; src/item_registry.nvgt:item_plural:quivers
msg.145ba8a7a0f2=quivers
; src/item_registry.nvgt:item_display:great_spears
msg.14941e07fc96=Great Spears
; src/bosses/bandit_hideout.nvgt:860:insert_last[0]
; src/bosses/bandit_hideout.nvgt:859:insert_last[0]
msg.14f5f5ff4f76=No survivors were found.
; seed:learn_sounds_label:sounds/game/game_falls.ogg
msg.16134a4250f7=game falls
@@ -782,7 +785,7 @@ msg.1bfa6bc809d4==== Bandit's Hideout ===
msg.1c7022965d1a=skin glove
; src/menus/base_info.nvgt:20:insert_last[0]
msg.1d15d0d5c98d=Storage built. Total items {arg1}
; src/bosses/bandit_hideout.nvgt:806:insert_last[0]
; src/bosses/bandit_hideout.nvgt:805:insert_last[0]
msg.1d61343b9660=Storage not built. No item rewards.
; seed:learn_sounds_label:sounds/items/fire.ogg
msg.1da840244989=fire
@@ -792,7 +795,7 @@ msg.1db76ac85135=sling
msg.237b5017397b=axe
; src/bosses/unicorn/unicorn_boss.nvgt:117:insert_last[0]
msg.245c89590526=- When the bridge collapses with the Unicorn on it, you win!
; src/bosses/bandit_hideout.nvgt:834:insert_last[0]
; src/bosses/bandit_hideout.nvgt:833:insert_last[0]
msg.25db7a4ec692=Livestock were recovered and added to your pasture.
; seed:learn_sounds_label:sounds/quests/safe_to_jump.ogg
msg.2630bd23d7d4=safe to jump
@@ -818,7 +821,7 @@ msg.27e4cdae51e3=vampyr4
msg.27f1dd42841b=- Press Enter to confirm
; src/menus/base_info.nvgt:30:insert_last[0]
msg.27f5254de319=Storage not built
; src/bosses/bandit_hideout.nvgt:817:insert_last[0]
; src/bosses/bandit_hideout.nvgt:816:insert_last[0]
msg.27f76f96867d={arg1}: +{arg2}.
; src/bosses/unicorn/unicorn_boss.nvgt:115:insert_last[0]
msg.282b619d84bc=- Or fight the Unicorn directly (it has massive health)
@@ -952,7 +955,7 @@ msg.5009c8e190ee=turkey
msg.508e8bc0cc57=- Use Up/Down to change the number
; seed:learn_sounds_label:sounds/weapons/sling_low_range.ogg
msg.5123c22ce6a5=sling low range
; draugnorak.nvgt:113:insert_last[0]
; draugnorak.nvgt:114:insert_last[0]
msg.528fb1c4e5fb=New Game
; seed:learn_sounds_label:sounds/quests/squish.ogg
msg.5460f2e3c43e=squish
@@ -980,7 +983,7 @@ msg.5b61e4982b59=wight dies
msg.5bdb7fa2223d=Feathers
; src/item_registry.nvgt:item_plural:reeds
msg.5c2434ecd90f=reeds
; src/bosses/bandit_hideout.nvgt:829:insert_last[0]
; src/bosses/bandit_hideout.nvgt:828:insert_last[0]
msg.5cac31311668=Pasture is full. No livestock were recovered.
; src/bosses/unicorn/unicorn_boss.nvgt:110:insert_last[0]
msg.5cc7429297c8=that can be destroyed with an axe.
@@ -993,7 +996,7 @@ msg.5ea84b1201bb={arg1} favor grants you the eyes of an eagle.
msg.5f3f3a85393b=menu select
; src/menus/base_info.nvgt:12:insert_last[0]
msg.623f78278373=Barricade health {arg1} of {arg2}
; src/bosses/bandit_hideout.nvgt:827:insert_last[0]
; src/bosses/bandit_hideout.nvgt:826:insert_last[0]
msg.62583aa87a59=Pasture or storage not built. No livestock were recovered.
; seed:learn_sounds_label:sounds/actions/climb_tree.ogg
msg.627a5b9e6322=climb tree
@@ -1070,8 +1073,8 @@ msg.7e83de91b4a2=bandit female dies
msg.7f7e3bbf8864=An enemy is no longer in range of your currently wielded weapon.
; src/item_registry.nvgt:item_display:backpacks
msg.7fae743693f4=Backpacks
; draugnorak.nvgt:232:ui_info_box[1]
; draugnorak.nvgt:246:ui_info_box[1]
; draugnorak.nvgt:233:ui_info_box[1]
; draugnorak.nvgt:247:ui_info_box[1]
msg.802aa6576577=Load Game
; src/item_registry.nvgt:item_plural:sticks
msg.803e4c41bbd2=sticks
@@ -1129,7 +1132,7 @@ msg.924e0eb6b6f1=player male damage
msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while.
; seed:learn_sounds_label:sounds/actions/climb_rope.ogg
msg.92abc6d6953f=climb rope
; src/bosses/bandit_hideout.nvgt:800:insert_last[0]
; src/bosses/bandit_hideout.nvgt:799:insert_last[0]
msg.9379874d592b=Altar not built. No favor awarded.
; src/bosses/unicorn/unicorn_boss.nvgt:113:insert_last[0]
msg.93894d088f6a=- Use your axe to destroy a bridge support
@@ -1138,7 +1141,7 @@ msg.93894d088f6a=- Use your axe to destroy a bridge support
msg.946466f6c963=stick
; seed:learn_sounds_label:sounds/game/boar.ogg
msg.957f554c342a=boar
; src/bosses/bandit_hideout.nvgt:857:insert_last[0]
; src/bosses/bandit_hideout.nvgt:856:insert_last[0]
msg.97c59037a63c=A survivor joins your base.
; src/item_registry.nvgt:item_plural:smoked_fish
; src/item_registry.nvgt:item_singular:smoked_fish
@@ -1147,7 +1150,7 @@ msg.995d4e06ae4e=smoked fish
msg.998b61086fa7=You picked up an item for which there is no specific sound.
; src/quests/catch_the_boomerang_game.nvgt:19:insert_last[0]
msg.9afdb7e61391=- Press Space to throw
; src/bosses/bandit_hideout.nvgt:865:text_reader_lines[1]
; src/bosses/bandit_hideout.nvgt:864:text_reader_lines[1]
msg.9b64062b2074=Bandit's Hideout
; seed:learn_sounds_label:sounds/enemies/bandit4.ogg
msg.9bbcfc6c8f9d=bandit4
@@ -1182,7 +1185,7 @@ msg.a56be51786dc=bone1
msg.a570b024087d=Ever hungry, they feed upon your residents.
; src/quests/escape_from_hel_game.nvgt:15:insert_last[0]
msg.a641f39a6317=- The run ends when you fall into a grave
; src/bosses/bandit_hideout.nvgt:798:insert_last[0]
; src/bosses/bandit_hideout.nvgt:797:insert_last[0]
msg.a646f0dcb735=Favor awarded: {arg1}.
; src/item_registry.nvgt:item_plural:slings
msg.a6caf0c3771d=slings
@@ -1233,7 +1236,7 @@ msg.b73a0709297f=call pet
msg.b774102c89b5=Knives
; seed:learn_sounds_label:sounds/terrain/snow.ogg
msg.b94e9f3d7e00=snow
; src/bosses/bandit_hideout.nvgt:844:insert_last[0]
; src/bosses/bandit_hideout.nvgt:843:insert_last[0]
msg.ba3af68702a3=Learned Rune of Destruction!
; src/item_registry.nvgt:item_display:baskets_of_fruits_and_nuts
msg.ba7d7dfe330b=Baskets of Fruits and Nuts
@@ -1250,7 +1253,7 @@ msg.bdebda25dad9=arrow hit
msg.bf93e5ce8bc1=Down
; src/sound_settings.nvgt:26:learn_sounds_add_description[1]
msg.c01409b3a728=When casting release control when over water. When catching release when sound is over player.
; src/bosses/bandit_hideout.nvgt:821:insert_last[0]
; src/bosses/bandit_hideout.nvgt:820:insert_last[0]
msg.c05feaca7269=No items were recovered.
; src/item_registry.nvgt:item_singular:snare
msg.c1104d82c0bd=snare
@@ -1266,7 +1269,7 @@ msg.c52888225c69=fall
msg.c65e5dfc5ba7=bandit1
; seed:learn_sounds_label:sounds/enemies/corpse_worm.ogg
msg.c6d56016b910=corpse worm
; src/bosses/bandit_hideout.nvgt:789:insert_last[0]
; src/bosses/bandit_hideout.nvgt:788:insert_last[0]
msg.c72b7844929d=Bandits defeated: {arg1}.
; seed:learn_sounds_label:sounds/terrain/stream.ogg
msg.c82e3d7279ef=stream
@@ -1278,7 +1281,7 @@ msg.c8da1c4f1714=Lowers in pitch as the fall progresses.
msg.c90399f41b3f=Meat
; seed:learn_sounds_label:sounds/quests/bone7.ogg
msg.c9532ab465fb=bone7
; src/bosses/bandit_hideout.nvgt:808:insert_last[0]
; src/bosses/bandit_hideout.nvgt:807:insert_last[0]
msg.cb8b9677952a=Storage rewards:
; src/item_registry.nvgt:item_plural:boar_carcasses
msg.cb8e70ab2bcc=boar carcasses
@@ -1298,7 +1301,7 @@ msg.ce3897c6b3be={arg1}: {arg2}
msg.ce5aeca95fb9=item breaks
; src/i18n.nvgt:677:speak_with_history[0]
; src/pet_system.nvgt:936:speak_with_history[0]
; src/save_system.nvgt:344:speak_with_history[0]
; src/save_system.nvgt:410:speak_with_history[0]
msg.cea88b214070={arg1} {arg2}
; seed:manual
msg.cfe2650b4c6e=Found a stone.
@@ -1327,7 +1330,7 @@ msg.d6dc139f529a=invasion
msg.d6fd3467a950=yeti1
; src/item_registry.nvgt:item_display:axes
msg.d75f36f88797=Axes
; src/bosses/bandit_hideout.nvgt:836:insert_last[0]
; src/bosses/bandit_hideout.nvgt:835:insert_last[0]
msg.d7879bd506af=No livestock were recovered.
; src/quests/enchanted_melody_game.nvgt:31:insert_last[0]
msg.da0b5a461d98=F or K - First note (lowest pitch)
@@ -1395,7 +1398,7 @@ msg.e5d79aba2048=undead resident1
msg.e5f656d506a7=zombie hit
; src/quests/escape_from_hel_game.nvgt:8:insert_last[0]
msg.e6526216c064=You have plundered the treasure, and the Draugr are displeased.!
; src/bosses/bandit_hideout.nvgt:847:insert_last[0]
; src/bosses/bandit_hideout.nvgt:846:insert_last[0]
msg.e65fa8960222=You have already mastered the Rune of Destruction.
; seed:learn_sounds_label:sounds/quests/bone8.ogg
msg.e6ae58426f13=bone8
@@ -1405,9 +1408,9 @@ msg.e7409fcb54bc=feed fire
msg.e8002c169040=wood
; src/quests/escape_from_hel_game.nvgt:13:insert_last[0]
msg.e8216720d30b=- Press SPACE to jump over graves
; draugnorak.nvgt:133:insert_last[0]
; draugnorak.nvgt:134:insert_last[0]
msg.e8db3a42f86f=Learn Sounds
; src/bosses/bandit_hideout.nvgt:845:insert_last[0]
; src/bosses/bandit_hideout.nvgt:844:insert_last[0]
msg.e8e8f4ee7a5a=You can now engrave weapons with this rune at the crafting menu.
; src/bosses/bandit_hideout.nvgt:302:insert_last[0]
msg.e9dd1d268b65=The base lies far to the east, guarded by a barricade.
@@ -1436,7 +1439,7 @@ msg.f3d964225e83=player female damage
msg.f5cabf873590=reed
; src/bosses/bandit_hideout.nvgt:301:insert_last[0]
msg.f77fc018cb7b=You find a hidden bandit base deep in the forest.
; draugnorak.nvgt:149:insert_last[0]
; draugnorak.nvgt:150:insert_last[0]
msg.f83b6fe3aebf=Exit
; src/inventory_items.nvgt:504:speak_with_history[0]
; src/quests/skeletal_bard_game.nvgt:26:speak_with_history[0]
+27 -24
View File
@@ -33,6 +33,8 @@ load_game.delete_save_failed=Unable to delete save.
load_game.save_deleted=Save deleted.
load_game.no_saves_found=No saves found.
load_game.unable_to_load=Unable to load save.
load_game.invalid_data=Save file is incomplete or contains invalid data.
load_game.unsupported_version=This save was created by an unsupported game version.
load_game.select_prompt=Load game. Select character. Press delete to remove a save.
quick_slot.no_item_bound=No item bound to slot {slot}.
menu.closed=Closed.
@@ -708,7 +710,7 @@ msg.041d8413712d=A crash log was written to your save data folder.
msg.04a8916a9494=vampyr1
; src/item_registry.nvgt:item_plural:incense
msg.0532db2192fa=incense
; src/bosses/bandit_hideout.nvgt:787:insert_last[0]
; src/bosses/bandit_hideout.nvgt:786:insert_last[0]
msg.066723a92e94==== Victory Rewards ===
; seed:learn_sounds_label:sounds/enemies/corpse_worm_dies.ogg
msg.06c9fefbd6d7=corpse worm dies
@@ -749,12 +751,13 @@ msg.0f9af12cbb82=skin pouch
; src/item_registry.nvgt:item_display:logs
msg.126dd3b70a5c=Logs
; libstorm-nvgt/file_viewer.nvgt:74:screen_reader_speak[0]
; src/save_system.nvgt:233:ui_info_box[1]
msg.136e4cb03bd5=Failed to save file
; src/item_registry.nvgt:item_plural:quivers
msg.145ba8a7a0f2=quivers
; src/item_registry.nvgt:item_display:great_spears
msg.14941e07fc96=Great Spears
; src/bosses/bandit_hideout.nvgt:860:insert_last[0]
; src/bosses/bandit_hideout.nvgt:859:insert_last[0]
msg.14f5f5ff4f76=No survivors were found.
; seed:learn_sounds_label:sounds/game/game_falls.ogg
msg.16134a4250f7=game falls
@@ -782,7 +785,7 @@ msg.1bfa6bc809d4==== Bandit's Hideout ===
msg.1c7022965d1a=skin glove
; src/menus/base_info.nvgt:20:insert_last[0]
msg.1d15d0d5c98d=Storage built. Total items {arg1}
; src/bosses/bandit_hideout.nvgt:806:insert_last[0]
; src/bosses/bandit_hideout.nvgt:805:insert_last[0]
msg.1d61343b9660=Storage not built. No item rewards.
; seed:learn_sounds_label:sounds/items/fire.ogg
msg.1da840244989=fire
@@ -792,7 +795,7 @@ msg.1db76ac85135=sling
msg.237b5017397b=axe
; src/bosses/unicorn/unicorn_boss.nvgt:117:insert_last[0]
msg.245c89590526=- When the bridge collapses with the Unicorn on it, you win!
; src/bosses/bandit_hideout.nvgt:834:insert_last[0]
; src/bosses/bandit_hideout.nvgt:833:insert_last[0]
msg.25db7a4ec692=Livestock were recovered and added to your pasture.
; seed:learn_sounds_label:sounds/quests/safe_to_jump.ogg
msg.2630bd23d7d4=safe to jump
@@ -818,7 +821,7 @@ msg.27e4cdae51e3=vampyr4
msg.27f1dd42841b=- Press Enter to confirm
; src/menus/base_info.nvgt:30:insert_last[0]
msg.27f5254de319=Storage not built
; src/bosses/bandit_hideout.nvgt:817:insert_last[0]
; src/bosses/bandit_hideout.nvgt:816:insert_last[0]
msg.27f76f96867d={arg1}: +{arg2}.
; src/bosses/unicorn/unicorn_boss.nvgt:115:insert_last[0]
msg.282b619d84bc=- Or fight the Unicorn directly (it has massive health)
@@ -952,7 +955,7 @@ msg.5009c8e190ee=turkey
msg.508e8bc0cc57=- Use Up/Down to change the number
; seed:learn_sounds_label:sounds/weapons/sling_low_range.ogg
msg.5123c22ce6a5=sling low range
; draugnorak.nvgt:113:insert_last[0]
; draugnorak.nvgt:114:insert_last[0]
msg.528fb1c4e5fb=New Game
; seed:learn_sounds_label:sounds/quests/squish.ogg
msg.5460f2e3c43e=squish
@@ -980,7 +983,7 @@ msg.5b61e4982b59=wight dies
msg.5bdb7fa2223d=Feathers
; src/item_registry.nvgt:item_plural:reeds
msg.5c2434ecd90f=reeds
; src/bosses/bandit_hideout.nvgt:829:insert_last[0]
; src/bosses/bandit_hideout.nvgt:828:insert_last[0]
msg.5cac31311668=Pasture is full. No livestock were recovered.
; src/bosses/unicorn/unicorn_boss.nvgt:110:insert_last[0]
msg.5cc7429297c8=that can be destroyed with an axe.
@@ -993,7 +996,7 @@ msg.5ea84b1201bb={arg1} favor grants you the eyes of an eagle.
msg.5f3f3a85393b=menu select
; src/menus/base_info.nvgt:12:insert_last[0]
msg.623f78278373=Barricade health {arg1} of {arg2}
; src/bosses/bandit_hideout.nvgt:827:insert_last[0]
; src/bosses/bandit_hideout.nvgt:826:insert_last[0]
msg.62583aa87a59=Pasture or storage not built. No livestock were recovered.
; seed:learn_sounds_label:sounds/actions/climb_tree.ogg
msg.627a5b9e6322=climb tree
@@ -1070,8 +1073,8 @@ msg.7e83de91b4a2=bandit female dies
msg.7f7e3bbf8864=An enemy is no longer in range of your currently wielded weapon.
; src/item_registry.nvgt:item_display:backpacks
msg.7fae743693f4=Backpacks
; draugnorak.nvgt:232:ui_info_box[1]
; draugnorak.nvgt:246:ui_info_box[1]
; draugnorak.nvgt:233:ui_info_box[1]
; draugnorak.nvgt:247:ui_info_box[1]
msg.802aa6576577=Load Game
; src/item_registry.nvgt:item_plural:sticks
msg.803e4c41bbd2=sticks
@@ -1129,7 +1132,7 @@ msg.924e0eb6b6f1=player male damage
msg.9281d2fe5f08={arg1} favor shines upon you. You feel swift for a while.
; seed:learn_sounds_label:sounds/actions/climb_rope.ogg
msg.92abc6d6953f=climb rope
; src/bosses/bandit_hideout.nvgt:800:insert_last[0]
; src/bosses/bandit_hideout.nvgt:799:insert_last[0]
msg.9379874d592b=Altar not built. No favor awarded.
; src/bosses/unicorn/unicorn_boss.nvgt:113:insert_last[0]
msg.93894d088f6a=- Use your axe to destroy a bridge support
@@ -1138,7 +1141,7 @@ msg.93894d088f6a=- Use your axe to destroy a bridge support
msg.946466f6c963=stick
; seed:learn_sounds_label:sounds/game/boar.ogg
msg.957f554c342a=boar
; src/bosses/bandit_hideout.nvgt:857:insert_last[0]
; src/bosses/bandit_hideout.nvgt:856:insert_last[0]
msg.97c59037a63c=A survivor joins your base.
; src/item_registry.nvgt:item_plural:smoked_fish
; src/item_registry.nvgt:item_singular:smoked_fish
@@ -1147,7 +1150,7 @@ msg.995d4e06ae4e=smoked fish
msg.998b61086fa7=You picked up an item for which there is no specific sound.
; src/quests/catch_the_boomerang_game.nvgt:19:insert_last[0]
msg.9afdb7e61391=- Press Space to throw
; src/bosses/bandit_hideout.nvgt:865:text_reader_lines[1]
; src/bosses/bandit_hideout.nvgt:864:text_reader_lines[1]
msg.9b64062b2074=Bandit's Hideout
; seed:learn_sounds_label:sounds/enemies/bandit4.ogg
msg.9bbcfc6c8f9d=bandit4
@@ -1182,7 +1185,7 @@ msg.a56be51786dc=bone1
msg.a570b024087d=Ever hungry, they feed upon your residents.
; src/quests/escape_from_hel_game.nvgt:15:insert_last[0]
msg.a641f39a6317=- The run ends when you fall into a grave
; src/bosses/bandit_hideout.nvgt:798:insert_last[0]
; src/bosses/bandit_hideout.nvgt:797:insert_last[0]
msg.a646f0dcb735=Favor awarded: {arg1}.
; src/item_registry.nvgt:item_plural:slings
msg.a6caf0c3771d=slings
@@ -1233,7 +1236,7 @@ msg.b73a0709297f=call pet
msg.b774102c89b5=Knives
; seed:learn_sounds_label:sounds/terrain/snow.ogg
msg.b94e9f3d7e00=snow
; src/bosses/bandit_hideout.nvgt:844:insert_last[0]
; src/bosses/bandit_hideout.nvgt:843:insert_last[0]
msg.ba3af68702a3=Learned Rune of Destruction!
; src/item_registry.nvgt:item_display:baskets_of_fruits_and_nuts
msg.ba7d7dfe330b=Baskets of Fruits and Nuts
@@ -1250,7 +1253,7 @@ msg.bdebda25dad9=arrow hit
msg.bf93e5ce8bc1=Down
; src/sound_settings.nvgt:26:learn_sounds_add_description[1]
msg.c01409b3a728=When casting release control when over water. When catching release when sound is over player.
; src/bosses/bandit_hideout.nvgt:821:insert_last[0]
; src/bosses/bandit_hideout.nvgt:820:insert_last[0]
msg.c05feaca7269=No items were recovered.
; src/item_registry.nvgt:item_singular:snare
msg.c1104d82c0bd=snare
@@ -1266,7 +1269,7 @@ msg.c52888225c69=fall
msg.c65e5dfc5ba7=bandit1
; seed:learn_sounds_label:sounds/enemies/corpse_worm.ogg
msg.c6d56016b910=corpse worm
; src/bosses/bandit_hideout.nvgt:789:insert_last[0]
; src/bosses/bandit_hideout.nvgt:788:insert_last[0]
msg.c72b7844929d=Bandits defeated: {arg1}.
; seed:learn_sounds_label:sounds/terrain/stream.ogg
msg.c82e3d7279ef=stream
@@ -1278,7 +1281,7 @@ msg.c8da1c4f1714=Lowers in pitch as the fall progresses.
msg.c90399f41b3f=Meat
; seed:learn_sounds_label:sounds/quests/bone7.ogg
msg.c9532ab465fb=bone7
; src/bosses/bandit_hideout.nvgt:808:insert_last[0]
; src/bosses/bandit_hideout.nvgt:807:insert_last[0]
msg.cb8b9677952a=Storage rewards:
; src/item_registry.nvgt:item_plural:boar_carcasses
msg.cb8e70ab2bcc=boar carcasses
@@ -1298,7 +1301,7 @@ msg.ce3897c6b3be={arg1}: {arg2}
msg.ce5aeca95fb9=item breaks
; src/i18n.nvgt:677:speak_with_history[0]
; src/pet_system.nvgt:936:speak_with_history[0]
; src/save_system.nvgt:344:speak_with_history[0]
; src/save_system.nvgt:410:speak_with_history[0]
msg.cea88b214070={arg1} {arg2}
; seed:manual
msg.cfe2650b4c6e=Found a stone.
@@ -1327,7 +1330,7 @@ msg.d6dc139f529a=invasion
msg.d6fd3467a950=yeti1
; src/item_registry.nvgt:item_display:axes
msg.d75f36f88797=Axes
; src/bosses/bandit_hideout.nvgt:836:insert_last[0]
; src/bosses/bandit_hideout.nvgt:835:insert_last[0]
msg.d7879bd506af=No livestock were recovered.
; src/quests/enchanted_melody_game.nvgt:31:insert_last[0]
msg.da0b5a461d98=F or K - First note (lowest pitch)
@@ -1395,7 +1398,7 @@ msg.e5d79aba2048=undead resident1
msg.e5f656d506a7=zombie hit
; src/quests/escape_from_hel_game.nvgt:8:insert_last[0]
msg.e6526216c064=You have plundered the treasure, and the Draugr are displeased.!
; src/bosses/bandit_hideout.nvgt:847:insert_last[0]
; src/bosses/bandit_hideout.nvgt:846:insert_last[0]
msg.e65fa8960222=You have already mastered the Rune of Destruction.
; seed:learn_sounds_label:sounds/quests/bone8.ogg
msg.e6ae58426f13=bone8
@@ -1405,9 +1408,9 @@ msg.e7409fcb54bc=feed fire
msg.e8002c169040=wood
; src/quests/escape_from_hel_game.nvgt:13:insert_last[0]
msg.e8216720d30b=- Press SPACE to jump over graves
; draugnorak.nvgt:133:insert_last[0]
; draugnorak.nvgt:134:insert_last[0]
msg.e8db3a42f86f=Learn Sounds
; src/bosses/bandit_hideout.nvgt:845:insert_last[0]
; src/bosses/bandit_hideout.nvgt:844:insert_last[0]
msg.e8e8f4ee7a5a=You can now engrave weapons with this rune at the crafting menu.
; src/bosses/bandit_hideout.nvgt:302:insert_last[0]
msg.e9dd1d268b65=The base lies far to the east, guarded by a barricade.
@@ -1436,7 +1439,7 @@ msg.f3d964225e83=player female damage
msg.f5cabf873590=reed
; src/bosses/bandit_hideout.nvgt:301:insert_last[0]
msg.f77fc018cb7b=You find a hidden bandit base deep in the forest.
; draugnorak.nvgt:149:insert_last[0]
; draugnorak.nvgt:150:insert_last[0]
msg.f83b6fe3aebf=Exit
; src/inventory_items.nvgt:504:speak_with_history[0]
; src/quests/skeletal_bard_game.nvgt:26:speak_with_history[0]
+2
View File
@@ -14,6 +14,8 @@ load_game.delete_save_failed=No se pudo eliminar la partida.
load_game.save_deleted=Partida eliminada.
load_game.no_saves_found=No hay partidas guardadas.
load_game.unable_to_load=No se pudo cargar la partida.
load_game.invalid_data=El archivo de guardado está incompleto o contiene datos no válidos.
load_game.unsupported_version=Esta partida se creó con una versión del juego no compatible.
load_game.select_prompt=Cargar partida. Selecciona personaje. Presiona Suprimir para borrar una partida.
main_menu.prompt=Draugnorak. Menu principal.
game.new_game_started=Partida nueva iniciada.
+2
View File
@@ -524,6 +524,8 @@ def write_catalog(entries: Dict[str, Dict[str, object]], output_path: Path) -> N
("system.load_game.save_deleted", "Save deleted."),
("system.load_game.no_saves_found", "No saves found."),
("system.load_game.unable_to_load", "Unable to load save."),
("system.load_game.invalid_data", "Save file is incomplete or contains invalid data."),
("system.load_game.unsupported_version", "This save was created by an unsupported game version."),
("system.load_game.select_prompt", "Load game. Select character. Press delete to remove a save."),
("system.quick_slot.no_item_bound", "No item bound to slot {slot}."),
("system.menu.closed", "Closed."),
+1 -2
View File
@@ -79,8 +79,6 @@ void run_adventure_menu(int player_x) {
play_menu_select_sound();
start_adventure(adventure_ids[selection]);
// Adventure has finished (it blocks until done)
// Resume main game ambience
update_ambience(true);
return;
}
}
@@ -95,4 +93,5 @@ void start_adventure(int adventure_id) {
run_bandit_hideout_adventure();
}
menuBackgroundUpdatesEnabled = true;
restore_world_audio();
}
+3 -4
View File
@@ -286,12 +286,12 @@ void cleanup_bandit_hideout_adventure() {
clear_hideout_bandits();
end_pet_adventure();
reset_adventure_combat_state();
p.destroy_all();
reset_game_sound_pool();
}
void run_bandit_hideout_adventure() {
// Stop main game sounds
p.destroy_all();
reset_game_sound_pool();
init_bandit_hideout_adventure();
@@ -317,13 +317,12 @@ void run_bandit_hideout_adventure() {
if (key_pressed(KEY_BACK)) {
adventurePaused = !adventurePaused;
set_gameplay_timers_paused(adventurePaused);
if (adventurePaused) {
p.pause_all();
speak_with_history(tr("system.game.paused_prompt"), true);
} else {
p.resume_all();
restart_hideout_adventure_timers();
restart_all_timers();
speak_with_history(tr("system.game.resumed"), true);
}
continue;
+3 -4
View File
@@ -84,7 +84,7 @@ void init_unicorn_adventure() {
}
void cleanup_unicorn_adventure() {
p.destroy_all();
reset_game_sound_pool();
reset_adventure_combat_state();
end_pet_adventure();
if (unicorn.sound_handle != -1) {
@@ -95,7 +95,7 @@ void cleanup_unicorn_adventure() {
void run_unicorn_adventure() {
// Stop main game sounds
p.destroy_all();
reset_game_sound_pool();
init_unicorn_adventure();
@@ -130,13 +130,12 @@ void run_unicorn_adventure() {
if (key_pressed(KEY_BACK)) {
adventurePaused = !adventurePaused;
set_gameplay_timers_paused(adventurePaused);
if (adventurePaused) {
p.pause_all();
speak_with_history(tr("system.game.paused_prompt"), true);
} else {
p.resume_all();
restart_unicorn_adventure_timers();
restart_all_timers();
speak_with_history(tr("system.game.resumed"), true);
}
continue;
+136
View File
@@ -0,0 +1,136 @@
// Keep elapsed gameplay progress when pausing in the world or either adventure.
timer@[] get_gameplay_timers() {
timer@[] timers = {
@walktimer, @jumptimer, @search_timer, @search_delay_timer, @attack_timer,
@fall_timer, @climb_timer, @rope_climb_timer, @fire_damage_timer, @healing_timer,
@sling_charge_timer, @bow_draw_timer, @bow_shot_timer, @fishing_timer, @cast_move_timer,
@blessing_speed_timer, @blessing_resident_timer, @blessing_search_timer,
@hour_timer, @crossfade_timer, @wind_gust_timer, @rain_fade_timer, @thunder_timer,
@corpse_worm_spawn_timer, @resident_combat_timer, @movementBlockTimer, @ammoBlockTimer,
@fylgjaChargeTimer, @petAttackTimer, @petRetrieveTimer, @petTravelTimer,
@notificationsDelayTimer, @arena_jump_timer, @arena_walk_timer, @arena_attack_timer,
@unicorn.move_timer, @hideoutWalkTimer, @hideoutAttackTimer, @hideoutSearchTimer,
@hideoutSearchDelayTimer, @hideoutJumpTimer
};
for (uint i = 0; i < trees.length(); i++)
timers.insert_last(@trees[i].regen_timer);
for (uint i = 0; i < world_fires.length(); i++)
timers.insert_last(@world_fires[i].fuel_timer);
for (uint i = 0; i < world_snares.length(); i++)
timers.insert_last(@world_snares[i].hour_timer);
for (uint i = 0; i < undeads.length(); i++) {
timers.insert_last(@undeads[i].move_timer);
timers.insert_last(@undeads[i].attack_timer);
}
for (uint i = 0; i < bandits.length(); i++) {
timers.insert_last(@bandits[i].move_timer);
timers.insert_last(@bandits[i].attack_timer);
timers.insert_last(@bandits[i].wander_direction_timer);
}
for (uint i = 0; i < ground_games.length(); i++) {
timers.insert_last(@ground_games[i].move_timer);
timers.insert_last(@ground_games[i].attack_timer);
}
for (uint i = 0; i < corpse_worms.length(); i++) {
timers.insert_last(@corpse_worms[i].move_timer);
timers.insert_last(@corpse_worms[i].attack_timer);
}
for (uint i = 0; i < flying_creatures.length(); i++) {
timers.insert_last(@flying_creatures[i].move_timer);
timers.insert_last(@flying_creatures[i].fall_timer);
timers.insert_last(@flying_creatures[i].fade_timer);
}
for (uint i = 0; i < active_thunder.length(); i++)
timers.insert_last(@active_thunder[i].movement_timer);
for (uint i = 0; i < hideoutBandits.length(); i++) {
timers.insert_last(@hideoutBandits[i].moveTimer);
timers.insert_last(@hideoutBandits[i].attackTimer);
}
return timers;
}
void set_gameplay_timers_paused(bool paused) {
timer@[] timers = get_gameplay_timers();
for (uint i = 0; i < timers.length(); i++)
timers[i].set_paused(paused);
}
// Every pool-wide destruction must invalidate the handles retained by owners.
void reset_game_sound_pool() {
p.destroy_all();
day_sound_handle = -1;
night_sound_handle = -1;
fall_sound_handle = -1;
rope_climb_sound_handle = -1;
sling_sound_handle = -1;
bow_shot_sound_handle = -1;
cast_sound_handle = -1;
fylgjaSoundHandle = -1;
currentFylgjaSound = "";
petEventSoundHandle = -1;
petTravelSoundHandle = -1;
playerItemBreakSoundHandle = -1;
wind_sound_handle = -1;
rain_sound_handle = -1;
for (uint i = 0; i < active_thunder.length(); i++)
active_thunder[i].sound_handle = -1;
active_thunder.resize(0);
for (uint i = 0; i < trees.length(); i++)
trees[i].sound_handle = -1;
for (uint i = 0; i < world_fires.length(); i++)
world_fires[i].sound_handle = -1;
for (uint i = 0; i < world_snares.length(); i++)
world_snares[i].sound_handle = -1;
for (uint i = 0; i < world_streams.length(); i++) {
world_streams[i].sound_handle = -1;
world_streams[i].sound_position = -1;
}
for (uint i = 0; i < world_mountains.length(); i++) {
world_mountains[i].stream_sound_handle = -1;
world_mountains[i].stream_sound_position = -1;
}
for (uint i = 0; i < world_drops.length(); i++)
world_drops[i].sound_handle = -1;
for (uint i = 0; i < undeads.length(); i++) {
undeads[i].sound_handle = -1;
undeads[i].in_weapon_range = false;
}
for (uint i = 0; i < bandits.length(); i++) {
bandits[i].sound_handle = -1;
bandits[i].in_weapon_range = false;
}
for (uint i = 0; i < ground_games.length(); i++) {
ground_games[i].sound_handle = -1;
ground_games[i].in_weapon_range = false;
}
for (uint i = 0; i < corpse_worms.length(); i++) {
corpse_worms[i].sound_handle = -1;
corpse_worms[i].in_weapon_range = false;
}
for (uint i = 0; i < flying_creatures.length(); i++) {
flying_creatures[i].sound_handle = -1;
flying_creatures[i].fall_sound_handle = -1;
flying_creatures[i].in_weapon_range = false;
}
unicorn.sound_handle = -1;
current_unicorn_sound = "";
unicorn_in_weapon_range = false;
for (uint i = 0; i < hideoutBandits.length(); i++) {
hideoutBandits[i].soundHandle = -1;
hideoutBandits[i].inWeaponRange = false;
}
}
void restore_world_audio() {
p.update_listener_1d(x);
crossfade_active = false;
is_daytime = is_day_hour(current_hour);
update_ambience(true);
rain_fading = false;
safe_destroy_sound(rain_sound_handle);
if (rain_intensity > INTENSITY_NONE) {
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
if (rain_sound_handle != -1)
p.update_sound_start_values(rain_sound_handle, 0.0, get_rain_target_volume(rain_intensity), 1.0);
}
}
-17
View File
@@ -99,20 +99,3 @@ int last_adventure_day = -1;
bool game_paused = false;
bool menuBackgroundUpdatesEnabled = true;
bool return_to_main_menu_requested = false;
// Restart all game timers when resuming from pause
// Prevents time from accumulating while paused
void restart_all_timers() {
// Player timers
fire_damage_timer.restart();
healing_timer.restart();
sling_charge_timer.restart();
bow_draw_timer.restart();
bow_shot_timer.restart();
fylgjaChargeTimer.restart();
// Fire fuel timers
for (uint i = 0; i < world_fires.length(); i++) {
world_fires[i].fuel_timer.restart();
}
}
+90 -25
View File
@@ -3,6 +3,7 @@
#include "libstorm-nvgt/dict_utils.nvgt"
#include "libstorm-nvgt/name_sanitize.nvgt"
#include "libstorm-nvgt/save_utils.nvgt"
#include "src/save_validation.nvgt"
const string SAVE_EXTENSION = ".dat";
const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1";
@@ -112,7 +113,7 @@ void migrate_legacy_save_files_if_needed() {
continue;
string targetPath = resolve_save_path(legacyName);
if (file_exists(targetPath))
if (file_exists(targetPath) || file_exists(targetPath + ".bak"))
continue;
file legacyFile;
@@ -130,6 +131,15 @@ void migrate_legacy_save_files_if_needed() {
string[] get_save_files() {
migrate_legacy_save_files_if_needed();
// A replacement may have stopped after moving the previous save aside.
string[] @backups = glob(resolve_save_path("*" + SAVE_EXTENSION + ".bak"));
if (@backups != null) {
for (uint i = 0; i < backups.length(); i++) {
string backupPath = backups[i];
recover_interrupted_save(backupPath.substr(0, backupPath.length() - 4));
}
}
string[] result;
string[] @items = glob(resolve_save_path("*" + SAVE_EXTENSION));
if (@items == null)
@@ -165,7 +175,63 @@ string decrypt_save_data(const string& in encryptedData) {
}
bool save_data(const string& in filename, const string& in data) {
return save_string_file(resolve_save_path(filename), data);
last_save_error = "";
string targetPath = resolve_save_path(filename);
string stagedPath = targetPath + ".tmp";
string backupPath = targetPath + ".bak";
if (filename == "" || data.length() == 0 || directory_exists(targetPath) ||
!recover_interrupted_save(filename)) {
return save_write_failed(targetPath);
}
// The shared writer only touches staging; the previous save stays intact.
string stagedData;
if (!save_string_file(stagedPath, data) || !read_string_file(stagedPath, stagedData) || stagedData != data) {
return save_write_failed(targetPath);
}
bool hadSave = file_exists(targetPath);
if (hadSave) {
if (directory_exists(backupPath) || (file_exists(backupPath) && !file_delete(backupPath)) ||
!file_move(targetPath, backupPath)) {
return save_write_failed(targetPath);
}
}
if (!file_move(stagedPath, targetPath)) {
if (hadSave)
recover_interrupted_save(filename);
return save_write_failed(targetPath);
}
return true;
}
bool save_write_failed(const string& in path) {
last_save_error = i18n_text("Failed to save file") + "\n" + path;
return false;
}
bool recover_interrupted_save(const string& in filename) {
string targetPath = resolve_save_path(filename);
if (file_exists(targetPath))
return true;
string backupPath = targetPath + ".bak";
if (!file_exists(backupPath))
return true;
return file_move(backupPath, targetPath);
}
bool delete_save_file(const string& in filename) {
string targetPath = resolve_save_path(filename);
string backupPath = targetPath + ".bak";
// Remove the recovery copy first so a deleted character cannot reappear.
if (file_exists(backupPath) && !file_delete(backupPath))
return false;
return file_delete(targetPath);
}
bool save_game_with_feedback() {
if (save_game_state())
return true;
ui_info_box(tr("system.ui.window_title"), i18n_text("Failed to save file"), last_save_error);
return false;
}
bool read_file_string(const string& in filename, string& out data) {
@@ -472,7 +538,7 @@ bool select_save_file(string& out filename) {
}
int confirm = ui_question("", prompt);
if (confirm == 1) {
if (file_delete(resolve_save_path(files[selection]))) {
if (delete_save_file(files[selection])) {
speak_with_history(tr("system.load_game.save_deleted"), true);
refreshList = true;
break;
@@ -561,6 +627,8 @@ void clear_world_objects() {
}
void reset_game_state() {
set_gameplay_timers_paused(false);
game_paused = false;
stop_active_sounds();
clear_world_objects();
@@ -721,7 +789,7 @@ void start_new_game() {
current_save_file = get_save_filename_for_name(player_name);
}
queue_current_goal_announcement(false);
save_game_state();
save_game_with_feedback();
}
string serialize_bool(bool value) {
@@ -830,7 +898,7 @@ void deserialize_inventory_array(const string& in data, int[] @arr, int expected
if (data.length() == 0)
return;
string[] @parts = data.split(",");
string[] parts = split_save_record(data, ",");
uint count = parts.length();
if (count > uint(expected_size))
count = uint(expected_size);
@@ -840,6 +908,7 @@ void deserialize_inventory_array(const string& in data, int[] @arr, int expected
}
bool save_game_state() {
last_save_error = "";
dictionary saveData;
saveData.set("version", SAVE_VERSION);
@@ -1092,6 +1161,10 @@ bool save_game_state() {
bool load_game_state_from_file(const string& in filename) {
last_save_error = "";
string resolvedFilename = resolve_save_path(filename);
if (!recover_interrupted_save(filename)) {
last_save_error = tr("system.load_game.unable_to_load");
return false;
}
if (!file_exists(resolvedFilename)) {
last_save_error = "No save file found.";
return false;
@@ -1112,16 +1185,8 @@ bool load_game_state_from_file(const string& in filename) {
last_save_error = "Save file is corrupted or unreadable.";
return false;
}
double version;
bool has_version = saveData.get("version", version);
if (!has_version) {
if (!has_number_key(saveData, "player_x") || !has_number_key(saveData, "player_health") ||
!has_number_key(saveData, "time_current_day")) {
last_save_error = "Save file is missing required data.";
return false;
}
version = 0;
}
if (!validate_save_data(saveData))
return false;
reset_game_state();
current_save_file = get_path_basename(filename);
@@ -1656,7 +1721,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] treeData = get_string_list_or_split(saveData, "trees_data");
for (uint i = 0; i < treeData.length(); i++) {
string[] @parts = treeData[i].split("|");
string[] parts = split_save_record(treeData[i]);
if (parts.length() < 8)
continue;
@@ -1676,7 +1741,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] snareData = get_string_list_or_split(saveData, "snares_data");
for (uint i = 0; i < snareData.length(); i++) {
string[] @parts = snareData[i].split("|");
string[] parts = split_save_record(snareData[i]);
if (parts.length() < 6)
continue;
@@ -1697,7 +1762,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] fireData = get_string_list_or_split(saveData, "fires_data");
for (uint i = 0; i < fireData.length(); i++) {
string[] @parts = fireData[i].split("|");
string[] parts = split_save_record(fireData[i]);
if (parts.length() < 3)
continue;
@@ -1763,7 +1828,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] streamData = get_string_list_or_split(saveData, "streams_data");
for (uint i = 0; i < streamData.length(); i++) {
string[] @parts = streamData[i].split("|");
string[] parts = split_save_record(streamData[i]);
if (parts.length() < 2)
continue;
@@ -1776,7 +1841,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] banditData = get_string_list_or_split(saveData, "bandits_data");
for (uint i = 0; i < banditData.length(); i++) {
string[] @parts = banditData[i].split("|");
string[] parts = split_save_record(banditData[i]);
if (parts.length() < 6)
continue;
@@ -1838,7 +1903,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] mountainData = get_string_list_or_split(saveData, "mountains_data");
for (uint i = 0; i < mountainData.length(); i++) {
string[] @parts = mountainData[i].split("|");
string[] parts = split_save_record(mountainData[i]);
if (parts.length() < 5)
continue;
@@ -1852,14 +1917,14 @@ bool load_game_state_from_file(const string& in filename) {
mountain.end_position = end_pos;
// Parse elevations
string[] @elev_parts = parts[2].split(",");
string[] elev_parts = split_save_record(parts[2], ",");
mountain.elevations.resize(elev_parts.length());
for (uint j = 0; j < elev_parts.length(); j++) {
mountain.elevations[j] = parse_int(elev_parts[j]);
}
// Parse terrain types
string[] @terrain_parts = parts[3].split(",");
string[] terrain_parts = split_save_record(parts[3], ",");
mountain.terrain_types.resize(terrain_parts.length());
for (uint j = 0; j < terrain_parts.length(); j++) {
mountain.terrain_types[j] = terrain_parts[j];
@@ -1867,7 +1932,7 @@ bool load_game_state_from_file(const string& in filename) {
// Parse stream positions
if (parts[4].length() > 0) {
string[] @stream_parts = parts[4].split(",");
string[] stream_parts = split_save_record(parts[4], ",");
for (uint j = 0; j < stream_parts.length(); j++) {
mountain.stream_positions.insert_last(parse_int(stream_parts[j]));
}
@@ -1878,7 +1943,7 @@ bool load_game_state_from_file(const string& in filename) {
string[] dropData = get_string_list_or_split(saveData, "drops_data");
for (uint i = 0; i < dropData.length(); i++) {
string[] @parts = dropData[i].split("|");
string[] parts = split_save_record(dropData[i]);
if (parts.length() < 2)
continue;
add_world_drop(parse_int(parts[0]), parts[1]);
+159
View File
@@ -0,0 +1,159 @@
// Validate before resetting the active character or allocating loaded arrays.
string[] split_save_record(const string& in encoded, const string& in delimiter = "|") {
string[] columns;
uint start = 0;
int separator = encoded.find(delimiter, start);
while (separator >= 0) {
columns.insert_last(encoded.substr(start, uint(separator) - start));
start = uint(separator) + delimiter.length();
separator = encoded.find(delimiter, start);
}
columns.insert_last(encoded.substr(start));
return columns;
}
bool save_integer_in_range(dictionary@ data, const string& in key, int minimum, int maximum, bool required = true) {
if (!data.exists(key))
return !required;
double value;
if (!data.get(key, value) || !(value >= minimum && value <= maximum))
return false;
return value == int(value);
}
bool save_integer_text_in_range(const string& in text, int minimum, int maximum) {
if (text.length() == 0 || text.length() > 11)
return false;
uint start = text.substr(0, 1) == "-" ? 1 : 0;
if (start == text.length())
return false;
for (uint i = start; i < text.length(); i++) {
string ch = text.substr(i, 1);
if (ch < "0" || ch > "9")
return false;
}
int64 value = parse_int(text);
return value >= minimum && value <= maximum;
}
bool invalid_save_data() {
last_save_error = tr("system.load_game.invalid_data");
return false;
}
bool validate_save_data(dictionary@ data) {
if (data.exists("version") && !save_integer_in_range(data, "version", 0, SAVE_VERSION)) {
last_save_error = tr("system.load_game.unsupported_version");
return false;
}
int version = int(get_number(data, "version", 0));
if (!save_integer_in_range(data, "world_map_size", 35, 2147483647, version >= 3))
return invalid_save_data();
int mapSize = int(get_number(data, "world_map_size", 35));
if (!save_integer_in_range(data, "player_x", 0, mapSize - 1) ||
!save_integer_in_range(data, "player_health", 0, 2147483647) ||
!save_integer_in_range(data, "time_current_day", 1, 2147483647) ||
!save_integer_in_range(data, "player_y", 0, 2147483647, version >= 3) ||
!save_integer_in_range(data, "player_facing", 0, 1, version >= 3) ||
!save_integer_in_range(data, "player_base_health", 1, 2147483647, version >= 3) ||
!save_integer_in_range(data, "player_max_health", 1, 2147483647, version >= 3) ||
!save_integer_in_range(data, "time_current_hour", 0, 23, version >= 3)) {
return invalid_save_data();
}
string[] inventoryKeys = {"personal_inventory", "storage_inventory"};
for (uint i = 0; i < inventoryKeys.length(); i++) {
if (!data.exists(inventoryKeys[i]) && version < 3)
continue;
string encoded;
if (!data.get(inventoryKeys[i], encoded))
return invalid_save_data();
string[] counts = split_save_record(encoded, ",");
// Great spears were appended to the version-3 registry after its release.
if (counts.length() < uint(ITEM_BASKET_FOOD + 1) || counts.length() > uint(ITEM_COUNT))
return invalid_save_data();
for (uint j = 0; j < counts.length(); j++) {
if (!save_integer_text_in_range(counts[j], 0, 2147483647))
return invalid_save_data();
}
}
string[] listKeys = {
"world_expanded_terrain_types", "trees_data", "snares_data", "fires_data",
"firepits_positions", "herb_gardens_positions", "storages_positions", "altars_positions",
"streams_data", "bandits_data", "mountains_data", "drops_data"
};
for (uint i = 0; i < listKeys.length(); i++) {
string encoded;
string[] records;
if (!data.get(listKeys[i], encoded) && !data.get(listKeys[i], records) &&
(data.exists(listKeys[i]) || version >= 3)) {
return invalid_save_data();
}
}
string[] terrain = get_string_list_or_split(data, "world_expanded_terrain_types");
if (terrain.length() != uint(mapSize - 35))
return invalid_save_data();
if (!save_integer_in_range(data, "world_expanded_area_start", mapSize == 35 ? -1 : 35,
mapSize == 35 ? -1 : 35, version >= 3) ||
!save_integer_in_range(data, "world_expanded_area_end", mapSize == 35 ? -1 : mapSize - 1,
mapSize == 35 ? -1 : mapSize - 1, version >= 3)) {
return invalid_save_data();
}
string[] positionKeys = {"firepits_positions", "herb_gardens_positions", "storages_positions",
"altars_positions", "pastures_positions", "stables_positions"};
for (uint i = 0; i < positionKeys.length(); i++) {
string[] positions = get_string_list_or_split(data, positionKeys[i]);
for (uint j = 0; j < positions.length(); j++) {
if (!save_integer_text_in_range(positions[j], 0, mapSize - 1))
return invalid_save_data();
}
}
string[] objectKeys = {"trees_data", "snares_data", "fires_data", "streams_data", "bandits_data", "drops_data"};
int[] minimumColumns = {8, 6, 3, 2, 6, 2};
for (uint i = 0; i < objectKeys.length(); i++) {
string[] records = get_string_list_or_split(data, objectKeys[i]);
for (uint j = 0; j < records.length(); j++) {
string[] columns = split_save_record(records[j]);
// Missed arrows in existing saves can have positions beyond map edges.
int minimumPosition = objectKeys[i] == "drops_data" ? -2147483647 : 0;
int maximumPosition = objectKeys[i] == "drops_data" ? 2147483647 : mapSize - 1;
if (columns.length() < uint(minimumColumns[i]) ||
!save_integer_text_in_range(columns[0], minimumPosition, maximumPosition)) {
return invalid_save_data();
}
if (objectKeys[i] == "streams_data" &&
!save_integer_text_in_range(columns[1], 1, mapSize - int(parse_int(columns[0])))) {
return invalid_save_data();
}
}
}
string[] mountains = get_string_list_or_split(data, "mountains_data");
for (uint i = 0; i < mountains.length(); i++) {
string[] columns = split_save_record(mountains[i]);
if (columns.length() < 5 || !save_integer_text_in_range(columns[0], 35, mapSize - 1) ||
!save_integer_text_in_range(columns[1], int(parse_int(columns[0])), mapSize - 1)) {
return invalid_save_data();
}
int size = int(parse_int(columns[1]) - parse_int(columns[0]) + 1);
string[] elevations = split_save_record(columns[2], ",");
string[] terrainTypes = split_save_record(columns[3], ",");
if (elevations.length() != uint(size) || terrainTypes.length() != uint(size))
return invalid_save_data();
for (uint j = 0; j < elevations.length(); j++) {
if (!save_integer_text_in_range(elevations[j], 0, MOUNTAIN_MAX_ELEVATION))
return invalid_save_data();
}
if (columns[4] != "") {
string[] streams = split_save_record(columns[4], ",");
for (uint j = 0; j < streams.length(); j++) {
if (!save_integer_text_in_range(streams[j], 0, size - 1))
return invalid_save_data();
}
}
}
return true;
}
+1 -1
View File
@@ -996,7 +996,7 @@ void update_time() {
attempt_resident_fish_smoking();
attempt_livestock_production();
if (current_hour == DAY_START_HOUR) {
save_game_state();
save_game_with_feedback();
}
}