diff --git a/include/libtrx/config/config.h b/include/libtrx/config/config.h new file mode 100644 index 0000000..5999263 --- /dev/null +++ b/include/libtrx/config/config.h @@ -0,0 +1,9 @@ +#pragma once + +#include "config_option.h" + +bool Config_Read(void); +bool Config_Write(void); +void Config_Sanitize(void); +void Config_ApplyChanges(void); +const CONFIG_OPTION *Config_GetOptionMap(void); diff --git a/include/libtrx/game/console/commands/config.h b/include/libtrx/game/console/commands/config.h new file mode 100644 index 0000000..3a552b9 --- /dev/null +++ b/include/libtrx/game/console/commands/config.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../../../config/config_option.h" +#include "../common.h" + +extern CONSOLE_COMMAND g_Console_Cmd_Config; + +const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromKey(const char *key); +const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget(const void *target); +COMMAND_RESULT Console_Cmd_Config_Helper( + const CONFIG_OPTION *option, const char *new_value); diff --git a/include/libtrx/game/console/commands/heal.h b/include/libtrx/game/console/commands/heal.h new file mode 100644 index 0000000..0f1fa0a --- /dev/null +++ b/include/libtrx/game/console/commands/heal.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../common.h" + +extern CONSOLE_COMMAND g_Console_Cmd_Heal; diff --git a/include/libtrx/game/game_string.def b/include/libtrx/game/game_string.def index f7e58e1..07c458b 100644 --- a/include/libtrx/game/game_string.def +++ b/include/libtrx/game/game_string.def @@ -1,3 +1,10 @@ GS_DEFINE(OSD_POS_GET, "Room: %d\nPosition: %.3f, %.3f, %.3f\nRotation: %.3f,%.3f,%.3f") GS_DEFINE(OSD_CURRENT_HEALTH_GET, "Current Lara's health: %d") GS_DEFINE(OSD_CURRENT_HEALTH_SET, "Lara's health set to %d") +GS_DEFINE(OSD_CONFIG_OPTION_GET, "%s is currently set to %s") +GS_DEFINE(OSD_CONFIG_OPTION_SET, "%s changed to %s") +GS_DEFINE(OSD_CONFIG_OPTION_UNKNOWN_OPTION, "Unknown option: %s") +GS_DEFINE(MISC_ON, "On") +GS_DEFINE(MISC_OFF, "Off") +GS_DEFINE(OSD_HEAL_ALREADY_FULL_HP, "Lara's already at full health") +GS_DEFINE(OSD_HEAL_SUCCESS, "Healed Lara back to full health") diff --git a/include/libtrx/game/lara/misc.h b/include/libtrx/game/lara/misc.h new file mode 100644 index 0000000..5c7ce0c --- /dev/null +++ b/include/libtrx/game/lara/misc.h @@ -0,0 +1,3 @@ +#pragma once + +void Lara_Extinguish(void); diff --git a/meson.build b/meson.build index ff434fd..4e3b290 100644 --- a/meson.build +++ b/meson.build @@ -65,6 +65,8 @@ sources = [ 'src/engine/image.c', 'src/enum_str.c', 'src/filesystem.c', + 'src/game/console/commands/config.c', + 'src/game/console/commands/heal.c', 'src/game/console/commands/pos.c', 'src/game/console/commands/set_health.c', 'src/game/items.c', diff --git a/src/game/console/commands/config.c b/src/game/console/commands/config.c new file mode 100644 index 0000000..8759628 --- /dev/null +++ b/src/game/console/commands/config.c @@ -0,0 +1,252 @@ +#include "game/console/commands/config.h" + +#include "config/config.h" +#include "config/config_map.h" +#include "game/game_string.h" +#include "memory.h" +#include "strings.h" + +#include +#include +#include + +static const char *Console_Cmd_Config_Resolve(const char *option_name); +static bool Console_Cmd_Config_SameKey(const char *key1, const char *key2); +static char *Console_Cmd_Config_NormalizeKey(const char *key); + +static bool Console_Cmd_Config_GetCurrentValue( + const CONFIG_OPTION *option, char *target, size_t target_size); +static bool Console_Cmd_Config_SetCurrentValue( + const CONFIG_OPTION *option, const char *new_value); + +static COMMAND_RESULT Console_Cmd_Set(const char *args); + +static const char *Console_Cmd_Config_Resolve(const char *const option_name) +{ + const char *dot = strrchr(option_name, '.'); + if (dot) { + return dot + 1; + } + return option_name; +} + +static bool Console_Cmd_Config_SameKey(const char *key1, const char *key2) +{ + key1 = Console_Cmd_Config_Resolve(key1); + key2 = Console_Cmd_Config_Resolve(key2); + const size_t len1 = strlen(key1); + const size_t len2 = strlen(key2); + if (len1 != len2) { + return false; + } + for (uint32_t i = 0; i < len1; i++) { + char c1 = key1[i]; + char c2 = key2[i]; + if (c1 == '_') { + c1 = '-'; + } + if (c2 == '_') { + c2 = '-'; + } + if (c1 != c2) { + return false; + } + } + return true; +} + +static char *Console_Cmd_Config_NormalizeKey(const char *key) +{ + // TODO: Once we support arbitrary glyphs, this conversion should + // no longer be necessary. + char *result = Memory_DupStr(key); + for (uint32_t i = 0; i < strlen(result); i++) { + if (result[i] == '_') { + result[i] = '-'; + } + } + return result; +} + +static bool Console_Cmd_Config_GetCurrentValue( + const CONFIG_OPTION *const option, char *target, const size_t target_size) +{ + if (option == NULL) { + return false; + } + + assert(option->target != NULL); + switch (option->type) { + case COT_BOOL: + snprintf( + target, target_size, "%s", + *(bool *)option->target ? GS(MISC_ON) : GS(MISC_OFF)); + break; + case COT_INT32: + snprintf(target, target_size, "%d", *(int32_t *)option->target); + break; + case COT_FLOAT: + snprintf(target, target_size, "%.2f", *(float *)option->target); + break; + case COT_DOUBLE: + snprintf(target, target_size, "%.2f", *(double *)option->target); + break; + case COT_ENUM: + for (const ENUM_STRING_MAP *enum_map = option->param; + enum_map->text != NULL; enum_map++) { + if (enum_map->value == *(int32_t *)option->target) { + strncpy(target, enum_map->text, target_size); + } + } + break; + } + return true; +} + +static bool Console_Cmd_Config_SetCurrentValue( + const CONFIG_OPTION *const option, const char *const new_value) +{ + if (option == NULL) { + return CR_BAD_INVOCATION; + } + + assert(option->target != NULL); + switch (option->type) { + case COT_BOOL: + if (String_Match(new_value, "on|true|1")) { + *(bool *)option->target = true; + return true; + } else if (String_Match(new_value, "off|false|0")) { + *(bool *)option->target = false; + return true; + } + break; + + case COT_INT32: { + int32_t new_value_typed; + if (sscanf(new_value, "%d", &new_value_typed) == 1) { + *(int32_t *)option->target = new_value_typed; + return true; + } + break; + } + + case COT_FLOAT: { + float new_value_typed; + if (sscanf(new_value, "%f", &new_value_typed) == 1) { + *(float *)option->target = new_value_typed; + return true; + } + break; + } + + case COT_DOUBLE: { + double new_value_typed; + if (sscanf(new_value, "%lf", &new_value_typed) == 1) { + *(double *)option->target = new_value_typed; + return true; + } + break; + } + + case COT_ENUM: + for (const ENUM_STRING_MAP *enum_map = option->param; + enum_map->text != NULL; enum_map++) { + if (String_Equivalent(enum_map->text, new_value)) { + *(int32_t *)option->target = enum_map->value; + return true; + } + } + break; + } + + return false; +} + +const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromKey(const char *const key) +{ + for (const CONFIG_OPTION *option = Config_GetOptionMap(); + option->name != NULL; option++) { + if (Console_Cmd_Config_SameKey(option->name, key)) { + return option; + } + } + + return NULL; +} + +const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget( + const void *const target) +{ + for (const CONFIG_OPTION *option = Config_GetOptionMap(); + option->name != NULL; option++) { + if (option->target == target) { + return option; + } + } + + return NULL; +} + +COMMAND_RESULT Console_Cmd_Config_Helper( + const CONFIG_OPTION *const option, const char *const new_value) +{ + assert(option != NULL); + + char *normalized_name = Console_Cmd_Config_NormalizeKey(option->name); + + COMMAND_RESULT result = CR_BAD_INVOCATION; + if (new_value == NULL || String_IsEmpty(new_value)) { + char cur_value[128]; + if (Console_Cmd_Config_GetCurrentValue(option, cur_value, 128)) { + Console_Log(GS(OSD_CONFIG_OPTION_GET), normalized_name, cur_value); + result = CR_SUCCESS; + } + } + + if (Console_Cmd_Config_SetCurrentValue(option, new_value)) { + Config_Sanitize(); + Config_Write(); + Config_ApplyChanges(); + + char final_value[128]; + assert(Console_Cmd_Config_GetCurrentValue(option, final_value, 128)); + Console_Log(GS(OSD_CONFIG_OPTION_SET), normalized_name, final_value); + result = CR_SUCCESS; + } + +cleanup: + Memory_FreePointer(&normalized_name); + return result; +} + +static COMMAND_RESULT Console_Cmd_Config(const char *const args) +{ + COMMAND_RESULT result = CR_BAD_INVOCATION; + + char *key = Memory_DupStr(args); + char *const space = strchr(key, ' '); + const char *new_value = NULL; + if (space != NULL) { + new_value = space + 1; + space[0] = '\0'; // NULL-terminate the key + } + + const CONFIG_OPTION *const option = + Console_Cmd_Config_GetOptionFromKey(key); + if (option == NULL) { + Console_Log(GS(OSD_CONFIG_OPTION_UNKNOWN_OPTION), key); + result = CR_FAILURE; + } else { + result = Console_Cmd_Config_Helper(option, new_value); + } + +cleanup: + Memory_FreePointer(&key); + return result; +} + +CONSOLE_COMMAND g_Console_Cmd_Config = { + .prefix = "set", + .proc = Console_Cmd_Config, +}; diff --git a/src/game/console/commands/heal.c b/src/game/console/commands/heal.c new file mode 100644 index 0000000..f687947 --- /dev/null +++ b/src/game/console/commands/heal.c @@ -0,0 +1,29 @@ +#include "game/console/commands/heal.h" + +#include "game/game.h" +#include "game/game_string.h" +#include "game/lara/common.h" +#include "game/lara/misc.h" + +static COMMAND_RESULT Console_Cmd_Heal(const char *const args) +{ + if (!Game_IsPlayable()) { + return CR_UNAVAILABLE; + } + + ITEM_INFO *const lara_item = Lara_GetItem(); + if (lara_item->hit_points == LARA_MAX_HITPOINTS) { + Console_Log(GS(OSD_HEAL_ALREADY_FULL_HP)); + return CR_SUCCESS; + } + + lara_item->hit_points = LARA_MAX_HITPOINTS; + Lara_Extinguish(); + Console_Log(GS(OSD_HEAL_SUCCESS)); + return CR_SUCCESS; +} + +CONSOLE_COMMAND g_Console_Cmd_Heal = { + .prefix = "heal", + .proc = Console_Cmd_Heal, +}; diff --git a/tools/additional_lint b/tools/additional_lint index 722353d..e2bf467 100755 --- a/tools/additional_lint +++ b/tools/additional_lint @@ -1,4 +1,5 @@ #!/usr/bin/env python3 from libtrx.cli.additional_lint import run_script +from libtrx.paths import LIBTRX_REPO_DIR -run_script(ignored_patterns=["*.patch", "*.bin", "gl_core_3_3.h"]) +run_script(root_dir=LIBTRX_REPO_DIR, ignored_patterns=["*.patch", "*.bin", "gl_core_3_3.h"]) diff --git a/tools/libtrx/cli/additional_lint.py b/tools/libtrx/cli/additional_lint.py index fbf954d..b429b1c 100644 --- a/tools/libtrx/cli/additional_lint.py +++ b/tools/libtrx/cli/additional_lint.py @@ -6,7 +6,7 @@ from pathlib import Path from libtrx.files import find_versioned_files, is_binary_file -from libtrx.linting import lint_file +from libtrx.linting import LintContext, lint_bulk_files, lint_file def parse_args() -> argparse.Namespace: @@ -37,22 +37,32 @@ def filter_files( def run_script( - root_dir: Path | None = None, ignored_patterns: list[str] | None = None + root_dir: Path, ignored_patterns: list[str] | None = None ) -> None: args = parse_args() + + context = LintContext( + root_dir=root_dir, + versioned_files=find_versioned_files(root_dir=root_dir), + ) if args.path: files = args.path else: - files = find_versioned_files(root_dir=root_dir) - - files = filter_files(files, ignored_patterns, debug=args.debug) + files = context.versioned_files + files = list(filter_files(files, ignored_patterns, debug=args.debug)) exit_code = 0 for file in files: if args.debug: print(f"Checking {file}...", file=sys.stderr) - for lint_warning in lint_file(file): + for lint_warning in lint_file(context, file): print(str(lint_warning), file=sys.stderr) exit_code = 1 + if args.debug: + print(f"Checking files in bulk {file}...", file=sys.stderr) + for lint_warning in lint_bulk_files(context, files): + print(str(lint_warning), file=sys.stderr) + exit_code = 1 + exit(exit_code) diff --git a/tools/libtrx/linting.py b/tools/libtrx/linting.py index 43b2c7b..2e490f3 100644 --- a/tools/libtrx/linting.py +++ b/tools/libtrx/linting.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 -import re import json +import re from collections.abc import Callable, Iterable from dataclasses import dataclass from pathlib import Path +@dataclass +class LintContext: + root_dir: Path + versioned_files: list[Path] + + @dataclass class LintWarning: path: Path @@ -19,7 +25,9 @@ def __str__(self) -> str: return f"{prefix}: {self.message}" -def lint_json_validity(path: Path) -> Iterable[LintWarning]: +def lint_json_validity( + context: LintContext, path: Path +) -> Iterable[LintWarning]: if path.suffix != ".json": return try: @@ -28,7 +36,7 @@ def lint_json_validity(path: Path) -> Iterable[LintWarning]: yield LintWarning(path, f"malformed JSON: {ex!s}") -def lint_newlines(path: Path) -> Iterable[LintWarning]: +def lint_newlines(context: LintContext, path: Path) -> Iterable[LintWarning]: text = path.read_text(encoding="utf-8") if not text: return @@ -38,7 +46,9 @@ def lint_newlines(path: Path) -> Iterable[LintWarning]: yield LintWarning(path, "extra newline character at end of file") -def lint_trailing_whitespace(path: Path) -> Iterable[LintWarning]: +def lint_trailing_whitespace( + context: LintContext, path: Path +) -> Iterable[LintWarning]: if path.suffix == ".md": return for i, line in enumerate(path.open("r"), 1): @@ -46,7 +56,9 @@ def lint_trailing_whitespace(path: Path) -> Iterable[LintWarning]: yield LintWarning(path, "trailing whitespace", line=i) -def lint_const_primitives(path: Path) -> Iterable[LintWarning]: +def lint_const_primitives( + context: LintContext, path: Path +) -> Iterable[LintWarning]: if path.suffix != ".h": return for i, line in enumerate(path.open("r"), 1): @@ -56,14 +68,64 @@ def lint_const_primitives(path: Path) -> Iterable[LintWarning]: yield LintWarning(path, "useless const", line=i) -ALL_LINTERS: list[Callable[[], Iterable[LintWarning]]] = [ +def lint_game_strings( + context: LintContext, paths: list[Path] +) -> Iterable[LintWarning]: + def_paths = list(context.root_dir.rglob("**/game_string.def")) + defs = [ + match.group(1) + for path in def_paths + for match in re.finditer( + r"GS_DEFINE\(([A-Z_]+),.*\)", path.read_text() + ) + ] + if not defs: + return + + path_hints = " or ".join( + str(path.relative_to(context.root_dir)) for path in def_paths + ) + + for path in paths: + if path.suffix != ".c": + continue + for i, line in enumerate(path.open("r"), 1): + if not (match := re.search(r"GS\(([A-Z_]+)\)", line)): + continue + + def_ = match.group(1) + if def_ in defs: + continue + + yield LintWarning( + path, + f"undefined game string: {def_}. " + f"Make sure it's defined in {path_hints}.", + i, + ) + + +ALL_LINTERS: list[Callable[[LintContext, Path], Iterable[LintWarning]]] = [ lint_json_validity, lint_newlines, lint_trailing_whitespace, lint_const_primitives, ] +ALL_BULK_LINTERS: list[ + Callable[[LintContext, list[Path]], Iterable[LintWarning]] +] = [ + lint_game_strings, +] + -def lint_file(file: Path) -> Iterable[LintWarning]: +def lint_file(context: LintContext, file: Path) -> Iterable[LintWarning]: for linter_func in ALL_LINTERS: - yield from linter_func(file) + yield from linter_func(context, file) + + +def lint_bulk_files( + context: LintContext, files: list[Path] +) -> Iterable[LintWarning]: + for linter_func in ALL_BULK_LINTERS: + yield from linter_func(context, files)