Texte, Tasten und Dateien

Alle Texte stehen in einer Tabelle mit Deutsch und Englisch, die das
Spiel zur Laufzeit liest. Die Tasten werden in Code angelegt, nach
physischer Position. SaveStore verwaltet den Spielerschlüssel dieser
Installation, schreibt den Speicherstand über eine temporäre Datei,
legt unbrauchbare Stände beiseite und behält die zehn neuesten
Aufzeichnungen.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-01 11:26:12 +02:00
parent 2607df8a97
commit ccf9813c80
16 changed files with 313 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
class_name I18n
extends RefCounted
## Loads the game's texts from a CSV file: the first column holds the key,
## every further column one language, named in the first row.
##
## The file is read at run time. Its import setting is "keep", so Godot
## generates no translation files from it.
const TEXTS_FILE := "res://i18n/texts.csv"
static var _installed := false
## Adds the translations of TEXTS_FILE to the TranslationServer, once.
static func install() -> void:
if _installed:
return
for translation in load_translations(TEXTS_FILE):
TranslationServer.add_translation(translation)
_installed = true
## One Translation per language column of the CSV file at `path`.
static func load_translations(path: String) -> Array[Translation]:
var translations: Array[Translation] = []
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
push_error("texts not found: %s" % path)
return translations
var header := file.get_csv_line()
for column in range(1, header.size()):
var translation := Translation.new()
translation.locale = header[column]
translations.append(translation)
while not file.eof_reached():
var row := file.get_csv_line()
if row.size() < header.size() or row[0].is_empty():
continue
for column in range(1, header.size()):
translations[column - 1].add_message(row[0], row[column])
return translations
+1
View File
@@ -0,0 +1 @@
uid://dy10r7pfqy63v
+35
View File
@@ -0,0 +1,35 @@
class_name InputSetup
extends RefCounted
## Registers the game's input actions in code, so that project.godot holds no
## serialized input events. Keys are physical positions, so WASD stays in
## place on German keyboards too.
static func install() -> void:
_add_keys("move_up", [KEY_W, KEY_UP])
_add_keys("move_down", [KEY_S, KEY_DOWN])
_add_keys("move_left", [KEY_A, KEY_LEFT])
_add_keys("move_right", [KEY_D, KEY_RIGHT])
_add_keys("save_game", [KEY_F5])
_add_keys("load_game", [KEY_F9])
_add_keys("toggle_pause", [KEY_ESCAPE])
_add_mouse_button("dig", MOUSE_BUTTON_LEFT)
static func _add_keys(action: StringName, keys: Array[Key]) -> void:
if InputMap.has_action(action):
return
InputMap.add_action(action)
for key in keys:
var event := InputEventKey.new()
event.physical_keycode = key
InputMap.action_add_event(action, event)
static func _add_mouse_button(action: StringName, button: MouseButton) -> void:
if InputMap.has_action(action):
return
InputMap.add_action(action)
var event := InputEventMouseButton.new()
event.button_index = button
InputMap.action_add_event(action, event)
+1
View File
@@ -0,0 +1 @@
uid://dvckt6ss6k4xi
+93
View File
@@ -0,0 +1,93 @@
class_name SaveStore
extends RefCounted
## The game's files on the player's machine: the player key, the save and
## the session recordings for bug reports.
const KEY_FILE := "player.key"
const SAVE_FILE := "world.grw"
const RECORDINGS_DIR := "recordings"
## Recordings beyond this number are deleted, oldest first.
const KEEP_RECORDINGS := 10
const KEY_BYTES := 16
var base_dir: String
func _init(directory: String = "user://") -> void:
base_dir = directory
DirAccess.make_dir_recursive_absolute(base_dir)
## The key that identifies this installation in every world. It is created
## on first use and kept from then on.
func player_key() -> PackedByteArray:
var path := base_dir.path_join(KEY_FILE)
var key := FileAccess.get_file_as_bytes(path)
if key.size() != KEY_BYTES:
key = Crypto.new().generate_random_bytes(KEY_BYTES)
_write(path, key)
return key
func has_save() -> bool:
return FileAccess.file_exists(base_dir.path_join(SAVE_FILE))
func read_save() -> PackedByteArray:
return FileAccess.get_file_as_bytes(base_dir.path_join(SAVE_FILE))
## Writes the save through a temporary file, so that a crash while writing
## never destroys the previous save. Returns false if nothing was written.
func write_save(bytes: PackedByteArray) -> bool:
if bytes.is_empty():
return false
var path := base_dir.path_join(SAVE_FILE)
var temporary := path + ".tmp"
if not _write(temporary, bytes):
return false
return DirAccess.rename_absolute(temporary, path) == OK
## Moves an unusable save out of the way, so that the next save does not
## overwrite it. An older set-aside save is replaced. Returns the new path.
func set_aside_save() -> String:
var path := base_dir.path_join(SAVE_FILE)
var aside := path + ".broken"
DirAccess.rename_absolute(path, aside)
return aside
## Stores a session recording and deletes the oldest ones beyond
## KEEP_RECORDINGS. Returns the path, or an empty text if nothing was written.
func write_recording(bytes: PackedByteArray) -> String:
if bytes.is_empty():
return ""
var directory := base_dir.path_join(RECORDINGS_DIR)
DirAccess.make_dir_recursive_absolute(directory)
# Microseconds since 1970, padded so that names sort by age.
var stamp := int(Time.get_unix_time_from_system() * 1_000_000.0)
var path := directory.path_join("session-%016d.grwr" % stamp)
if not _write(path, bytes):
return ""
var names := recordings()
while names.size() > KEEP_RECORDINGS:
DirAccess.remove_absolute(directory.path_join(names[0]))
names.remove_at(0)
return path
## File names of the stored recordings, oldest first.
func recordings() -> PackedStringArray:
var names := DirAccess.get_files_at(base_dir.path_join(RECORDINGS_DIR))
names.sort()
return names
func _write(path: String, bytes: PackedByteArray) -> bool:
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
return false
file.store_buffer(bytes)
file.close()
return true
+1
View File
@@ -0,0 +1 @@
uid://cbp4jkbdfyawm