tools: move to libtrx; remove memleaks patch

This commit is contained in:
Marcin Kurczewski 2024-04-30 00:37:50 +02:00
parent 32bf0eddf3
commit 6d71455dd8
55 changed files with 124 additions and 928 deletions

View file

@ -10,4 +10,3 @@
<string>icon</string>
</dict>
</plist>

View file

@ -4,7 +4,6 @@
#include "game/input.h"
#include "game/music.h"
#include "game/sound.h"
#include "gfx/context.h"
#include "global/const.h"
#include "global/types.h"
@ -14,6 +13,7 @@
#include <libtrx/memory.h>
#include <libtrx/utils.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

View file

@ -1,12 +1,12 @@
// IWYU pragma: no_include <bits/types/struct_tm.h>
#include "game/clock.h"
#include "config.h"
#include "game/console.h"
#include "game/game_string.h"
#include "game/phase/phase.h"
#include "global/vars.h"
#include "global/const.h"
#include <libtrx/log.h>
#include <libtrx/utils.h>
#include <assert.h>
#include <math.h>

View file

@ -10,7 +10,6 @@
#include "game/items.h"
#include "game/lara.h"
#include "game/lara/lara_cheat.h"
#include "game/los.h"
#include "game/objects/common.h"
#include "game/objects/names.h"
#include "game/output.h"
@ -21,13 +20,10 @@
#include "global/const.h"
#include "global/types.h"
#include "global/vars.h"
#include "math/math.h"
#include <libtrx/memory.h>
#include <libtrx/strings.h>
#include <libtrx/utils.h>
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>

View file

@ -1,17 +1,17 @@
#include "game/interpolation.h"
#include "game/clock.h"
#include "config.h"
#include "game/effects.h"
#include "game/items.h"
#include "game/lara/lara_hair.h"
#include "game/phase/phase.h"
#include "game/room.h"
#include "global/const.h"
#include "global/vars.h"
#include "math/math_misc.h"
#include <libtrx/utils.h>
#include <assert.h>
#include <stdint.h>
#define REMEMBER(target, member) (target)->interp.prev.member = (target)->member

View file

@ -2,6 +2,8 @@
#include "global/types.h"
#include <stdbool.h>
bool Interpolation_IsEnabled(void);
void Interpolation_Disable(void);
void Interpolation_Enable(void);

View file

@ -1,7 +1,6 @@
#include "game/inventory/inventory_vars.h"
#include "game/phase/phase.h"
#include "global/types.h"
#include "global/vars.h"
#include <stdbool.h>
#include <stdint.h>

View file

@ -1,7 +1,6 @@
#include "game/inventory/inventory_ring.h"
#include "config.h"
#include "game/clock.h"
#include "game/game_string.h"
#include "game/inventory.h"
#include "game/inventory/inventory_vars.h"

View file

@ -4,7 +4,6 @@
#include "game/anim.h"
#include "game/camera.h"
#include "game/carrier.h"
#include "game/clock.h"
#include "game/interpolation.h"
#include "game/room.h"
#include "game/shell.h"

View file

@ -3,14 +3,12 @@
#include "config.h"
#include "game/camera.h"
#include "game/collide.h"
#include "game/console.h"
#include "game/gun.h"
#include "game/input.h"
#include "game/inventory.h"
#include "game/items.h"
#include "game/lara/lara_cheat.h"
#include "game/lara/lara_control.h"
#include "game/los.h"
#include "game/lot.h"
#include "game/music.h"
#include "game/objects/common.h"

View file

@ -16,6 +16,8 @@
#include "global/types.h"
#include "global/vars.h"
#include <libtrx/utils.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

View file

@ -3,10 +3,8 @@
#include "config.h"
#include "game/box.h"
#include "game/collide.h"
#include "game/gameflow.h"
#include "game/gun.h"
#include "game/input.h"
#include "game/inventory.h"
#include "game/items.h"
#include "game/lara.h"
#include "game/lara/lara_cheat.h"
@ -14,7 +12,6 @@
#include "game/lara/lara_look.h"
#include "game/lara/lara_state.h"
#include "game/lot.h"
#include "game/objects/general/door.h"
#include "game/room.h"
#include "game/sound.h"
#include "global/const.h"

View file

@ -5,7 +5,6 @@
#include "game/lara/lara_hair.h"
#include "game/output.h"
#include "game/viewport.h"
#include "global/const.h"
#include "global/vars.h"
#include "math/matrix.h"

View file

@ -2,6 +2,9 @@
#include "global/types.h"
#include <stdbool.h>
#include <stdint.h>
typedef struct HAIR_SEGMENT {
XYZ_32 pos;
XYZ_16 rot;

View file

@ -27,6 +27,7 @@
#include <libtrx/filesystem.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <libtrx/utils.h>
#include <assert.h>
#include <stdio.h>

View file

@ -15,6 +15,7 @@
#include <libtrx/utils.h>
#include <assert.h>
#include <stddef.h>
const GAME_OBJECT_ID g_EnemyObjects[] = {
// clang-format off

View file

@ -2,7 +2,6 @@
#include "game/input.h"
#include "game/inventory.h"
#include "game/inventory/inventory_vars.h"
#include "game/items.h"
#include "game/lara.h"
#include "game/sound.h"

View file

@ -2,14 +2,14 @@
#include "game/input.h"
#include "game/inventory.h"
#include "game/inventory/inventory_vars.h"
#include "game/items.h"
#include "game/lara.h"
#include "game/objects/general/keyhole.h"
#include "game/sound.h"
#include "global/const.h"
#include "global/vars.h"
#include <stdbool.h>
#define LF_USEPUZZLE 80
static XYZ_32 m_PuzzleHolePosition = { .x = 0,

View file

@ -2,6 +2,8 @@
#include "global/types.h"
#include <stdint.h>
// Return a list of object ids that match given string.
// out_match_count may be NULL.
// The result must be freed by the caller.

View file

@ -9,6 +9,8 @@
#include "global/const.h"
#include "global/vars.h"
#include <stdbool.h>
#define EXTRA_ANIM_PLACE_BAR 0
#define EXTRA_ANIM_DIE_GOLD 1
#define LF_PICKUP_GOLD_BAR 113

View file

@ -1,6 +1,5 @@
#include "game/phase/phase.h"
#include "config.h"
#include "game/clock.h"
#include "game/interpolation.h"
#include "game/output.h"

View file

@ -1,6 +1,5 @@
#include "game/phase/phase_cutscene.h"
#include "config.h"
#include "game/camera.h"
#include "game/effects.h"
#include "game/game.h"

View file

@ -8,7 +8,6 @@
#include "game/gameflow.h"
#include "game/input.h"
#include "game/interpolation.h"
#include "game/inventory.h"
#include "game/items.h"
#include "game/lara.h"
#include "game/lara/lara_cheat.h"
@ -26,6 +25,8 @@
#include "global/types.h"
#include "global/vars.h"
#include <libtrx/utils.h>
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>

View file

@ -19,7 +19,8 @@
#include "global/types.h"
#include "global/vars.h"
#include <assert.h>
#include <libtrx/utils.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

View file

@ -13,7 +13,6 @@
#include "game/music.h"
#include "game/objects/common.h"
#include "game/option.h"
#include "game/option/option_compass.h"
#include "game/output.h"
#include "game/overlay.h"
#include "game/savegame.h"

View file

@ -2,7 +2,6 @@
#include "game/game.h"
#include "game/game_string.h"
#include "game/gameflow.h"
#include "game/input.h"
#include "game/interpolation.h"
#include "game/music.h"

View file

@ -15,6 +15,7 @@
#include "global/vars.h"
#include <libtrx/bson.h>
#include <libtrx/json.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <libtrx/utils.h>

View file

@ -4,6 +4,7 @@
#include "gfx/gl/gl_core_3_3.h"
#include "gfx/gl/utils.h"
#include "gfx/screenshot.h"
#include "gfx_options.h"
#include <libtrx/log.h>
#include <libtrx/memory.h>

View file

@ -2,6 +2,7 @@
#include "game/shell.h"
#include "gfx/gl/utils.h"
#include "gfx_options.h"
#include <libtrx/filesystem.h>
#include <libtrx/log.h>

View file

@ -15,7 +15,6 @@
#include "game/effect_routines/sand.h"
#include "game/effect_routines/stairs2slope.h"
#include "game/effect_routines/turn_180.h"
#include "game/gameflow.h"
#include <stddef.h>

View file

@ -13,6 +13,8 @@
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <libavcodec/version.h>
#define SDL_MAIN_HANDLED
#ifdef _WIN32
@ -27,7 +29,6 @@
#include <SDL2/SDL_mouse.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_video.h>
#include <libavcodec/version.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

@ -1 +1 @@
Subproject commit b1d1e82e5ce3fd47d746b78209733a11d7ef8996
Subproject commit c43b761b702012017a76248d34cde5e5af929b1b

View file

@ -1 +0,0 @@
../subprojects/libtrx/tools/additional_lint

4
tools/additional_lint Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env python3
from libtrx.cli.additional_lint import run_script
run_script(ignored_extensions=[".patch", ".bin"])

View file

@ -1,160 +0,0 @@
#!/usr/bin/env python3
"""This script can be used to analyze TR1X for potential memory leaks.
For the script to work, the repository needs to have applied
tools/analyze_memleaks.patch that logs information about every memory
allocation and free.
"""
import argparse
import re
from collections import defaultdict
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from enum import Enum
from itertools import groupby
from pathlib import Path
from typing import Any, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class OperationType(Enum):
Allocation = 1
Reallocation = 2
Deallocation = 3
@dataclass
class Operation:
file: str
line: int
func: str
type: OperationType
address: int
size: int | None
new_address: int | None = None
@dataclass
class Problem:
message: str
operation: Operation | None
def group(
source: Iterable[T], key=Callable[[T], U]
) -> Iterable[tuple[U, list[T]]]:
for key, group in groupby(sorted(source, key=key), key=key):
yield key, list(group)
def get_operations_from_log_lines(lines: Iterable[str]) -> Iterable[Operation]:
for line in lines:
if match := re.search(
r"(\S+) (\d+) (\S+) Allocating memory \((\d+) bytes\): ([A-F0-9]*)",
line,
):
yield Operation(
file=match.group(1),
line=int(match.group(2)),
func=match.group(3),
type=OperationType.Allocation,
size=int(match.group(4)),
address=int(match.group(5), 16),
)
elif match := re.search(
r"(\S+) (\d+) (\S+) Reallocating memory at ([A-F0-9]*) \((\d+) bytes\): ([A-F0-9]*)",
line,
):
yield Operation(
file=match.group(1),
line=int(match.group(2)),
func=match.group(3),
type=OperationType.Reallocation,
address=int(match.group(4), 16),
size=int(match.group(5)),
new_address=int(match.group(6), 16),
)
elif match := re.search(
r"(\S+) (\d+) (\S+) Freeing memory at ([A-F0-9]*)", line
):
yield Operation(
file=match.group(1),
line=int(match.group(2)),
func=match.group(3),
type=OperationType.Deallocation,
size=None,
address=int(match.group(4), 16),
)
def find_problems(operations: Iterable[Operation]) -> Iterable[Problem]:
reachable_memory: dict[int, Operation] = {}
for operation in operations:
if operation.type == OperationType.Allocation:
assert operation.address not in reachable_memory
reachable_memory[operation.address] = operation
elif operation.type == OperationType.Reallocation:
if operation.address != 0:
del reachable_memory[operation.address]
assert operation.new_address not in reachable_memory
reachable_memory[operation.new_address] = operation
elif operation.type == OperationType.Deallocation:
if operation.address != 0:
try:
del reachable_memory[operation.address]
except KeyError:
yield Problem("Invalid free", operation=operation)
for address, operation in reachable_memory.items():
yield Problem(
f"Unfreed memory: {address:08x} ({operation.size} bytes)",
operation=operation,
)
reachable_bytes = sum(
operation.size for operation in reachable_memory.values()
)
yield Problem(
f"Total reachable memory: {reachable_bytes / 1024.0 / 1024.0:.02f} MB",
operation=None,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("path", help="Path to the TR1X.log file.")
return parser.parse_args()
def main():
args = parse_args()
with Path(args.path).open("r", encoding="utf-8") as handle:
operations = list(get_operations_from_log_lines(handle))
key: tuple[str, int]
for key, values in group(
source=find_problems(operations),
key=(
lambda problem: (
problem.operation.file,
problem.operation.line,
)
if problem.operation
else ("ZZZ", 0)
),
):
operation = values[0].operation
if operation:
print(operation.file, operation.line)
else:
print("General problems:")
for problem in values:
print(" " * 4, problem.message)
if __name__ == "__main__":
main()

View file

@ -1,89 +0,0 @@
diff --git a/src/memory.c b/src/memory.c
index c73c24cf..fbc704f7 100644
--- a/src/memory.c
+++ b/src/memory.c
@@ -1,14 +1,16 @@
#include "memory.h"
#include "game/shell.h"
+#include "log.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
-void *Memory_Alloc(size_t size)
+void *Memory_AllocImpl(size_t size, const char *file, int line, const char *func)
{
void *result = malloc(size);
+ Log_Message(file, line, func, "Allocating memory (%d bytes): %p", size, result);
if (!result) {
Shell_ExitSystem("ERROR: Could not allocate enough memory");
}
@@ -16,33 +18,35 @@ void *Memory_Alloc(size_t size)
return result;
}
-void *Memory_Realloc(void *memory, size_t size)
+void *Memory_ReallocImpl(void *memory, size_t size, const char *file, int line, const char *func)
{
void *result = realloc(memory, size);
+ Log_Message(file, line, func, "Reallocating memory at %p (%d bytes): %p", memory, size, result);
if (!result) {
Shell_ExitSystem("ERROR: Could not allocate enough memory");
}
return result;
}
-void Memory_Free(void *memory)
+void Memory_FreeImpl(void *memory, const char *file, int line, const char *func)
{
+ Log_Message(file, line, func, "Freeing memory at %p", memory);
if (memory) {
free(memory);
}
}
-void Memory_FreePointer(void *arg)
+void Memory_FreePointerImpl(void *arg, const char *file, int line, const char *func)
{
assert(arg);
void *memory;
memcpy(&memory, arg, sizeof(void *));
memcpy(arg, &(void *) { NULL }, sizeof(void *));
- Memory_Free(memory);
+ Memory_FreeImpl(memory, file, line, func);
}
-char *Memory_DupStr(const char *string)
+char *Memory_DupStrImpl(const char *string, const char *file, int line, const char *func)
{
- char *memory = Memory_Alloc(strlen(string) + 1);
+ char *memory = Memory_AllocImpl(strlen(string) + 1, file, line, func);
strcpy(memory, string);
return memory;
}
diff --git a/src/memory.h b/src/memory.h
index 23c23f95..3995bada 100644
--- a/src/memory.h
+++ b/src/memory.h
@@ -5,8 +5,14 @@
#include <stddef.h>
-void *Memory_Alloc(size_t size);
-void *Memory_Realloc(void *memory, size_t size);
-void Memory_Free(void *memory);
-void Memory_FreePointer(void *memory);
-char *Memory_DupStr(const char *string);
+void *Memory_AllocImpl(size_t size, const char *file, int line, const char *func);
+void *Memory_ReallocImpl(void *memory, size_t size, const char *file, int line, const char *func);
+void Memory_FreeImpl(void *memory, const char *file, int line, const char *func);
+void Memory_FreePointerImpl(void *memory, const char *file, int line, const char *func);
+char *Memory_DupStrImpl(const char *string, const char *file, int line, const char *func);
+
+#define Memory_Alloc(size) (Memory_AllocImpl(size, __FILE__, __LINE__, __func__))
+#define Memory_Realloc(memory, size) (Memory_ReallocImpl(memory, size, __FILE__, __LINE__, __func__))
+#define Memory_Free(memory) (Memory_FreeImpl(memory, __FILE__, __LINE__, __func__))
+#define Memory_FreePointer(memory) (Memory_FreePointerImpl(memory, __FILE__, __LINE__, __func__))
+#define Memory_DupStr(string) (Memory_DupStrImpl(string, __FILE__, __LINE__, __func__))

View file

@ -3,11 +3,10 @@ import argparse
from pathlib import Path
import pyjson5
from tr1x.paths import TR1X_TOOLS_DIR
TOOLS_DIR = Path(__file__).parent
REPO_DIR = TOOLS_DIR.parent
CONFIG_TOOL_SPEC_PATH = (
TOOLS_DIR / "config/TR1X_ConfigTool/Resources/specification.json"
TR1X_TOOLS_DIR / "config/TR1X_ConfigTool/Resources/specification.json"
)
@ -36,16 +35,22 @@ def main() -> None:
}
for key, spec_value in spec_map.items():
if key in game_config and (game_value := game_config.get(key)) != spec_value:
print(f'(!) Wrong value: {key} (tool supplies {spec_value}, game supplies {game_value})')
if (
key in game_config
and (game_value := game_config.get(key)) != spec_value
):
print(
f"(!) Wrong value: {key} (tool supplies {spec_value}, game supplies {game_value})"
)
for key, spec_value in spec_map.items():
if key not in game_config:
print(f'Surplus key: {key}')
print(f"Surplus key: {key}")
for key, spec_value in game_config.items():
if key not in spec_map:
print(f'Missing key: {key}')
print(f"Missing key: {key}")
if __name__ == "__main__":
main()

View file

@ -1,20 +1,17 @@
#!/usr/bin/env python3
from pathlib import Path
from shared.docker import BaseGameEntrypoint
from libtrx.cli.game_docker_entrypoint import run_script
class LinuxEntrypoint(BaseGameEntrypoint):
BUILD_ROOT = Path("/app/build/linux/")
COMPILE_ARGS = []
RELEASE_ZIP_SUFFIX = "Linux"
RELEASE_ZIP_FILES = [
(BUILD_ROOT / "TR1X", "TR1X"),
]
def post_compile(self) -> None:
if self.target == "release":
self.compress_exe(self.BUILD_ROOT / "TR1X")
if __name__ == "__main__":
LinuxEntrypoint().run()
run_script(
ship_dir=Path("/app/data/ship/"),
build_root=Path("/app/build/linux/"),
compile_args=[],
release_zip_filename="TR1X-{version}-Linux.zip",
release_zip_files=[
(Path("/app/build/linux/TR1X"), "TR1X"),
],
compressable_exes=[
Path("/app/build/linux/TR1X"),
],
)

View file

@ -1,26 +1,24 @@
#!/usr/bin/env python3
from pathlib import Path
from shared.docker import BaseGameEntrypoint
from libtrx.cli.game_docker_entrypoint import run_script
class WindowsEntrypoint(BaseGameEntrypoint):
BUILD_ROOT = Path("/app/build/win/")
COMPILE_ARGS = [
run_script(
ship_dir=Path("/app/data/ship/"),
build_root=Path("/app/build/win/"),
compile_args=[
"--cross",
"/app/tools/docker/game-win/meson_linux_mingw32.txt",
]
RELEASE_ZIP_SUFFIX = "Windows"
RELEASE_ZIP_FILES = [
(BUILD_ROOT / "TR1X.exe", "TR1X.exe"),
(Path("/app/tools/config/out/TR1X_ConfigTool.exe"), "TR1X_ConfigTool.exe"),
]
def post_compile(self) -> None:
if self.target == "release":
for path in self.BUILD_ROOT.glob("*.exe"):
self.compress_exe(path)
if __name__ == "__main__":
WindowsEntrypoint().run()
],
release_zip_filename="TR1X-{version}-Windows.zip",
release_zip_files=[
(Path("/app/build/win/TR1X.exe"), "TR1X.exe"),
(
Path("/app/tools/config/out/TR1X_ConfigTool.exe"),
"TR1X_ConfigTool.exe",
),
],
compressable_exes=[
Path("/app/build/win/TR1X.exe"),
],
)

View file

@ -1,100 +0,0 @@
#!/usr/bin/env python3
# regenerate the .ICO file from .PSD.
import argparse
import tempfile
from dataclasses import dataclass
from pathlib import Path
from subprocess import check_call
@dataclass
class IconSpec:
size: int
type: str
SPECS = [
IconSpec(size=32, type="bmp"),
IconSpec(size=16, type="bmp"),
IconSpec(size=256, type="png"),
IconSpec(size=128, type="png"),
IconSpec(size=64, type="png"),
IconSpec(size=48, type="png"),
IconSpec(size=32, type="png"),
IconSpec(size=16, type="png"),
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("path", type=Path)
parser.add_argument("-o", "--output", type=Path, required=True)
return parser.parse_args()
def resize_transformer(path: Path, spec: IconSpec) -> None:
check_call(
[
"convert",
f"{path}[0]",
"-filter",
"lanczos",
"-resize",
f"{spec.size}x{spec.size}",
f'PNG:{path}',
]
)
def quantize_transformer(path: Path, spec: IconSpec) -> None:
quantized_path = path.with_stem(f"{path.stem}-quantized")
check_call(["pngquant", path, "--output", quantized_path])
path.write_bytes(quantized_path.read_bytes())
quantized_path.unlink()
def optimize_transformer(path: Path, spec: IconSpec) -> None:
check_call(["zopflipng", "-y", path, path])
def convert_transformer(path: Path, spec: IconSpec) -> None:
if spec.type != "png":
check_call(["convert", path, f'{spec.type.upper()}:{path}'])
TRANSFORMERS = [
resize_transformer,
quantize_transformer,
optimize_transformer,
convert_transformer,
]
def generate_icon(source_path: Path, target_path: Path) -> None:
aux_paths = []
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
for spec in SPECS:
aux_path = tmp_path / f"{spec.size}-{spec.type}.tmp"
aux_path.write_bytes(source_path.read_bytes())
for transform in TRANSFORMERS:
transform(aux_path, spec)
aux_paths.append(aux_path)
# NOTE: image order is important for certain software.
check_call(["identify", *aux_paths])
check_call(["convert", *aux_paths, target_path])
def main() -> None:
args = parse_args()
if args.output.exists():
args.output.unlink()
generate_icon(args.path, args.output)
if __name__ == "__main__":
main()

View file

@ -3,10 +3,10 @@ import argparse
from pathlib import Path
from subprocess import check_call
from common import DATA_DIR
from tr1x.paths import TR1X_DATA_DIR
SOURCE_DIR = DATA_DIR / "images"
TARGET_DIR = DATA_DIR / "ship/data/images"
SOURCE_DIR = TR1X_DATA_DIR / "images"
TARGET_DIR = TR1X_DATA_DIR / "ship/data/images"
def format_size(size: int) -> str:

View file

@ -2,7 +2,7 @@
import argparse
from pathlib import Path
from shared.versioning import generate_version
from libtrx.versioning import generate_version
TEMPLATE = """
const char *g_TR1XVersion = "TR1X {version}";

View file

@ -2,8 +2,8 @@
import argparse
from pathlib import Path
from shared.common import DATA_DIR
from shared.versioning import generate_version
from libtrx.versioning import generate_version
from tr1x.paths import TR1X_DATA_DIR
def parse_args() -> argparse.Namespace:
@ -17,7 +17,7 @@ def write_rc_template(
) -> None:
template = input_path.read_text()
template = template.replace("{version}", version)
template = template.replace("{icon_path}", str(DATA_DIR / "icon.ico"))
template = template.replace("{icon_path}", str(TR1X_DATA_DIR / "icon.ico"))
output_path.write_text(template)
@ -27,7 +27,7 @@ def main() -> None:
for output_path in args.output:
write_rc_template(
input_path=DATA_DIR / output_path.name,
input_path=TR1X_DATA_DIR / output_path.name,
output_path=output_path,
version=version,
)

View file

@ -1,25 +1,4 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
from subprocess import run
from libtrx.versioning import generate_version
from shared.versioning import generate_version
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("-o", "--output", type=Path)
return parser.parse_args()
def main() -> None:
args = parse_args()
version = generate_version()
if args.output:
args.output.write_text(version)
else:
print(version, end="")
if __name__ == "__main__":
main()
print(generate_version(), end="")

View file

@ -1,29 +0,0 @@
#!/usr/bin/env python3
import argparse
import json
import struct
import zlib
from pathlib import Path
import bson
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("path", type=Path)
return parser.parse_args()
def main() -> None:
args = parse_args()
with args.path.open("rb") as handle:
magic = handle.read(4)
version, compressed_size, uncompressed_size = struct.unpack(
"III", handle.read(12)
)
data = bson.loads(zlib.decompress(handle.read(uncompressed_size)))
print(json.dumps(data, indent=4))
if __name__ == "__main__":
main()

1
tools/libtrx Symbolic link
View file

@ -0,0 +1 @@
../subprojects/libtrx/tools/libtrx

View file

@ -1,28 +1,5 @@
#!/usr/bin/env python3
import re
from pathlib import Path
from libtrx.changelog import get_current_version_changelog
from tr1x.paths import TR1X_REPO_DIR
TOOLS_DIR = Path(__file__).parent
ROOT_DIR = TOOLS_DIR.parent
CHANGELOG_PATH = ROOT_DIR / "CHANGELOG.md"
def get_current_changelog() -> str:
sections = [
section
for section in CHANGELOG_PATH.read_text().split("\n\n")
if re.search(r"- \w", section)
]
if sections:
section = sections[0]
return "\n".join(
line for line in section.splitlines() if not line.startswith("#")
)
def main() -> None:
print(get_current_changelog())
if __name__ == "__main__":
main()
print(get_current_version_changelog(TR1X_REPO_DIR / "CHANGELOG.md"))

View file

@ -1,165 +1,8 @@
#!/usr/bin/env python3
import argparse
import re
import sys
from datetime import datetime
from pathlib import Path
from subprocess import check_output, run
from libtrx.cli.release import run_script
from tr1x.paths import TR1X_REPO_DIR
from shared.common import REPO_DIR
from shared.versioning import get_branch_version
HEADER = "## [Unreleased](https://github.com/LostArtefacts/TR1X/compare/stable...develop) - ××××-××-××"
CHANGELOG_PATH = REPO_DIR / "CHANGELOG.md"
def update_changelog(
changelog: str, old_version: str, new_version: str
) -> str:
if f"[{new_version}]" in changelog:
return changelog
changelog = re.sub("Unreleased", new_version, changelog, count=1)
changelog = re.sub("stable", old_version, changelog, count=1)
changelog = re.sub("develop", new_version, changelog, count=1)
changelog = re.sub(
"××××-××-××", datetime.now().strftime("%Y-%m-%d"), changelog
)
changelog = HEADER + "\n\n" + changelog
return changelog
class Git:
def checkout_branch(self, branch_name: str) -> None:
if check_output(["git", "diff", "--cached", "--name-only"]):
raise RuntimeError("Staged files")
check_output(["git", "checkout", branch_name])
def reset(self, target: str, hard: bool = False) -> None:
check_output(
["git", "reset", "develop", *(["--hard"] if hard else [])]
)
def delete_tag(self, tag_name: str) -> None:
run(["git", "tag", "-d", tag_name])
def create_tag(self, tag_name: str) -> None:
check_output(["git", "tag", tag_name])
def add(self, target: str) -> None:
check_output(["git", "add", target])
def commit(self, message: str) -> None:
check_output(["git", "commit", "-m", message])
def push(
self, upstream: str, targets: list[str], force: bool = False
) -> None:
check_output(
[
"git",
"push",
upstream,
*targets,
*(["--force-with-lease"] if force else []),
]
)
class BaseCommand:
name: str = NotImplemented
help: str = NotImplemented
def __init__(self, git: Git) -> None:
self.git = git
def decorate_parser(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument("version")
def run(self, args: argparse.Namespace) -> None:
raise NotImplementedError("not implemented")
class CommitCommand(BaseCommand):
name = "commit"
help = "Create and tag a commit with the release information"
def decorate_parser(self, parser: argparse.ArgumentParser) -> None:
super().decorate_parser(parser)
parser.add_argument(
"-d",
"--dry-run",
action='store_true',
help="only output the changelog to stdout, do not commit anything",
)
def run(self, args: argparse.Namespace) -> None:
self.git.checkout_branch("develop")
old_version = get_branch_version("origin/stable")
new_version = args.version
old_changelog = CHANGELOG_PATH.read_text()
new_changelog = update_changelog(
old_changelog, old_version, args.version
)
if old_changelog == new_changelog:
return
if args.dry_run:
print(new_changelog)
return
CHANGELOG_PATH.write_text(new_changelog)
self.git.add(CHANGELOG_PATH)
self.git.commit(f"docs: release {args.version}")
self.git.delete_tag(args.version)
self.git.create_tag(args.version)
class BranchCommand(BaseCommand):
name = "branch"
help = "Merge branch to the specified tag"
def run(self, args: argparse.Namespace) -> None:
self.git.checkout_branch("stable")
self.git.reset(args.version, hard=True)
self.git.checkout_branch("develop")
class PushCommand(BaseCommand):
name = "push"
help = (
"Push the develop and stable branches, and the version tag to GitHub"
)
def run(self, args) -> None:
self.git.push(
"origin", ["develop", "stable", args.version], force=True
)
def parse_args(commands: list[BaseCommand]) -> None:
parser = argparse.ArgumentParser(
description="Argument parser with subcommands"
)
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
for command in commands:
subparser = subparsers.add_parser(command.name, help=command.help)
command.decorate_parser(subparser)
subparser.set_defaults(command=command)
result = parser.parse_args()
if not hasattr(result, "command"):
parser.error("missing command")
return result
def main() -> None:
git = Git()
commands = [
command_cls(git) for command_cls in BaseCommand.__subclasses__()
]
args = parse_args(commands)
args.command.run(args)
main()
run_script(
project_name="TR1X",
changelog_path=TR1X_REPO_DIR / "CHANGELOG.md",
)

View file

@ -1,6 +0,0 @@
from pathlib import Path
TOOLS_DIR = Path(__file__).parent.parent
REPO_DIR = TOOLS_DIR.parent
DATA_DIR = REPO_DIR / "data"
SRC_DIR = REPO_DIR / "src"

View file

@ -1,92 +0,0 @@
import argparse
import os
from pathlib import Path
from subprocess import check_call, run
from shared.common import DATA_DIR
from shared.packaging import create_zip
from shared.versioning import generate_version
SHIP_DIR = DATA_DIR / "ship"
class BaseGameEntrypoint:
BUILD_ROOT: Path = ...
COMPILE_ARGS: list[str] = ...
STRIP_TOOL = "strip"
UPX_TOOL = "upx"
RELEASE_ZIP_SUFFIX: str = ...
RELEASE_ZIP_FILES: list[tuple[Path, str]] = ...
def __init__(self) -> None:
self.target = os.environ.get("TARGET", "debug")
def run(self) -> None:
args = self.parse_args()
args.func(args)
def parse_args(self) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Docker entrypoint")
subparsers = parser.add_subparsers(dest="action", help="Subcommands")
compile_parser = subparsers.add_parser(
"compile", help="Compile action"
)
compile_parser.set_defaults(func=self.compile)
package_parser = subparsers.add_parser(
"package", help="Package action"
)
package_parser.add_argument("-o", "--output", type=Path)
package_parser.set_defaults(func=self.package)
args = parser.parse_args()
if not hasattr(args, "func"):
args.action = "compile"
args.func = self.compile
return args
def compile(self, args: argparse.Namespace) -> None:
pkg_config_path = os.environ["PKG_CONFIG_PATH"]
if not Path("/app/build/linux/build.jinja").exists():
check_call(
[
"meson",
"--buildtype",
self.target,
*self.COMPILE_ARGS,
self.BUILD_ROOT,
"--pkg-config-path",
pkg_config_path,
]
)
check_call(["meson", "compile"], cwd=self.BUILD_ROOT)
self.post_compile()
def post_compile(self) -> None:
pass
def compress_exe(self, path: Path) -> None:
if run([self.UPX_TOOL, "-t", str(path)]).returncode != 0:
check_call([self.STRIP_TOOL, str(path)])
check_call([self.UPX_TOOL, str(path)])
def package(self, args: argparse.Namespace) -> None:
if args.output:
zip_path = args.output
else:
version = generate_version()
zip_path = Path(f"TR1X-{version}-{self.RELEASE_ZIP_SUFFIX}.zip")
source_files = [
*[
(path, path.relative_to(SHIP_DIR))
for path in SHIP_DIR.rglob("*")
if path.is_file()
],
*self.RELEASE_ZIP_FILES,
]
create_zip(zip_path, source_files)
print(f"Created {zip_path}")

View file

@ -1,15 +0,0 @@
import sys
import zipfile
from collections.abc import Iterable
from pathlib import Path
def create_zip(
output_path: Path, source_files: Iterable[tuple[Path, str]]
) -> None:
with zipfile.ZipFile(output_path, "w") as handle:
for source_path, archive_name in source_files:
if not source_path.exists():
print(f"WARNING: {source_path} does not exist", file=sys.stderr)
continue
handle.write(source_path, archive_name)

View file

@ -1,27 +0,0 @@
from subprocess import run
from shared.common import SRC_DIR
def get_branch_version(branch: str | None) -> str:
return run(
[
"git",
"describe",
*([branch] if branch else ["--dirty"]),
"--always",
"--abbrev=7",
"--tags",
"--exclude",
"latest",
],
cwd=SRC_DIR,
text=True,
capture_output=True,
check=False,
).stdout.strip()
def generate_version() -> str:
version = get_branch_version(None)
return version or "?"

View file

@ -1,41 +1,16 @@
#!/usr/bin/env python3
import argparse
import re
from pathlib import Path
from shutil import which
from subprocess import run
from libtrx.cli.sort_imports import run_script
from tr1x.paths import TR1X_REPO_DIR, TR1X_SRC_DIR
from shared.common import SRC_DIR
def fix_imports(path: Path) -> None:
iwyu_result = run(
["include-what-you-use", "-I", "src", path],
capture_output=True,
text=True,
).stderr
run(
[which("fix_include") and "fix_include" or "iwyu-fix-includes"],
input=iwyu_result,
text=True,
)
def custom_sort(source: list[str], forced_order: list[str]) -> list[str]:
def key_func(item: str) -> tuple[int, int, str]:
if item in forced_order:
return (forced_order[0], forced_order.index(item))
return (item, 0)
return sorted(source, key=key_func)
def sort_imports(path: Path) -> None:
source = path.read_text()
rel_path = path.relative_to(SRC_DIR)
own_include = str(rel_path.with_suffix(".h"))
own_include = {
# files headers of which are not a 1:1 match with their filename
run_script(
root_dir=TR1X_SRC_DIR,
include_dirs=[
TR1X_SRC_DIR,
TR1X_REPO_DIR / "build/linux",
TR1X_REPO_DIR / "build/windows",
],
system_include_dirs=[TR1X_REPO_DIR / "subprojects/libtrx/include"],
own_include_map={
"game/game/game.c": "game/game.h",
"game/game/game_cutscene.c": "game/game.h",
"game/game/game_demo.c": "game/game.h",
@ -50,77 +25,8 @@ def sort_imports(path: Path) -> None:
"game/savegame/savegame.c": "game/savegame.h",
"specific/s_audio_sample.c": "specific/s_audio.h",
"specific/s_audio_stream.c": "specific/s_audio.h",
"specific/s_log_unknown.c": "specific/s_log.h",
"specific/s_log_linux.c": "specific/s_log.h",
"specific/s_log_windows.c": "specific/s_log.h",
}.get(str(rel_path), own_include)
forced_order = [
"<windows.h>",
"<dbghelp.h>",
"<tlhelp32.h>",
]
def cb(match):
includes = re.findall(r'#include (["<][^"<>]+[">])', match.group(0))
groups = {
"self": set(),
"local": set(),
"shared": set(),
"external": set(),
}
for include in includes:
if include.strip('"') == own_include:
groups["self"].add(include)
elif include.startswith("<libtrx"):
groups["shared"].add(include)
elif include.startswith("<"):
groups["external"].add(include)
elif include.startswith('"'):
groups["local"].add(include)
groups = {key: value for key, value in groups.items() if value}
ret = "\n\n".join(
"\n".join(
f"#include {include}"
for include in custom_sort(group, forced_order)
)
for group in groups.values()
).strip()
return ret
source = re.sub(
"^#include [^\n]+(\n*#include [^\n]+)*",
cb,
source,
flags=re.M,
)
if source != path.read_text():
path.write_text(source)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(metavar="path", type=Path, nargs="*", dest="paths")
return parser.parse_args()
def main() -> None:
args = parse_args()
paths = [path.absolute() for path in args.paths]
if not paths:
paths = sorted(
path
for path in SRC_DIR.glob("**/*.[ch]")
if path != SRC_DIR / "init.c"
)
for path in paths:
fix_imports(path)
sort_imports(path)
if __name__ == "__main__":
main()
"specific/s_shell.c": "specific/s_shell.h",
},
fix_map={"libavcodec/version_major.h": "libavcodec/version.h"},
forced_order=[],
)

6
tools/tr1x/paths.py Normal file
View file

@ -0,0 +1,6 @@
from pathlib import Path
TR1X_TOOLS_DIR = Path(__file__).parent.parent
TR1X_REPO_DIR = TR1X_TOOLS_DIR.parent
TR1X_DATA_DIR = TR1X_REPO_DIR / "data"
TR1X_SRC_DIR = TR1X_REPO_DIR / "src"

View file

@ -2,10 +2,10 @@
import json
import re
from shared.common import DATA_DIR, SRC_DIR
from tr1x.paths import TR1X_DATA_DIR, TR1X_SRC_DIR
SHIP_DIR = DATA_DIR / "ship"
GAME_STRING_DEF_PATH = SRC_DIR / "game/game_string.def"
SHIP_DIR = TR1X_DATA_DIR / "ship"
GAME_STRING_DEF_PATH = TR1X_SRC_DIR / "game/game_string.def"
def get_default_string_map() -> dict[str, str]: