hotkeys moved to mods and checked across all enabled mods (apply refuses two hotkey options on the same key), agent briefs in notes/dev
This commit is contained in:
1 parent
1c8b766454
commit
86fe26d786
10 files changed
+203
-41
No files matched your search
@@ -0,0 +1,54 @@
|
|||||||
|
# Common rules for LlamaMod mod agents (read fully)
|
||||||
|
|
||||||
|
Project: ~/projects/llamamod — Java 25 Swing mod manager "LlamaMod" for the PC game "Ein Königreich für ein Lama"
|
||||||
|
(Emperor's New Groove, 2000, Argonaut/Croc-2 engine, 32-bit DirectX 6/7, German version). Git repo: DO NOT commit.
|
||||||
|
|
||||||
|
WORK IN YOUR OWN COPY (4 agents work in parallel; a half-written file in the shared tree would break the others' builds):
|
||||||
|
rsync -a --exclude build --exclude .gradle --exclude re/proj --exclude out ~/projects/llamamod/ $CLAUDE_JOB_DIR/tmp/<agent>/llamamod/
|
||||||
|
(re/groove_decomp.c and re/groove.exe come along; Ghidra project: copy re/proj too if you need headless Ghidra:
|
||||||
|
~/opt/ghidra_12.2_PUBLIC, JAVA_HOME=~/opt/jdk-25.0.5+8; never run it on ~/projects/llamamod/re/proj directly.)
|
||||||
|
Build/test there: ./gradlew spotlessApply build fatJar. When DONE, copy back ONLY your own new package directories
|
||||||
|
(src/main/java/.../mods/<yourpkg>/, src/test/java/.../mods/<yourpkg>/, resources under your package) plus your notes
|
||||||
|
file into ~/projects/llamamod, then run `./gradlew spotlessApply build` in ~/projects/llamamod once (if it fails because of
|
||||||
|
ANOTHER agent's in-progress package, re-run later; never edit other agents' packages). Your mod classes are ALREADY
|
||||||
|
registered in mods/ModRegistry.java (reflection, skipped if absent) – do not edit ModRegistry. Avoid editing shared
|
||||||
|
framework files (Mod, ModContext, ModManager, ModOption(s), exe/*, ui/*, platform/*). If you truly need a framework change,
|
||||||
|
keep it minimal and backwards compatible, and list it explicitly in your report (it must then also be copied back; tell me).
|
||||||
|
|
||||||
|
READ: notes/llamamod-app.md (framework: options API, categories, ExeImage/code caves/X86 emitter, "Testing mods in game"
|
||||||
|
with tools/apptest.py), notes/geometry.md, notes/objects.md, notes/strat_types_table.md, notes/levelbau.md, notes/debugviz.md.
|
||||||
|
Format API: package com.eliasmueller.llamamod.format (Wad, Level, MapChunk, Trak/Model/Vertex/Face, TextChunk,
|
||||||
|
Collision, Geometry, Stpc, Strats, Lin3, PyMath). Examples of mods: mods/debug/DebugWireframeMod, mods/testworld/TestWorldMod.
|
||||||
|
Decompile of groove.exe: re/groove_decomp.c (grep, don't read whole). Save format: save/Ksv.java.
|
||||||
|
|
||||||
|
LANGUAGE: everything English (identifiers, comments, UI labels, messages). Only necessary comments (offsets, patch bytes,
|
||||||
|
non-obvious engine facts). Each mod: id, name, description, category (Mod.LEVELS/GAMEPLAY/VISUAL/CHEATS), options().
|
||||||
|
|
||||||
|
IN-GAME TESTING is mandatory: tools/apptest.py from YOUR copy (it uses your copy's fat jar), with YOUR display number
|
||||||
|
(given below) and session names prefixed with your agent name. It runs a private PulseAudio (silent) – never start the
|
||||||
|
game any other way. Never use display :0, never touch ~/.wine-lama or the user's saves, never `pkill -f` patterns
|
||||||
|
(they match your own shell) – use PIDs / apptest stop. Stop your sessions when done. Look at screenshots with Read
|
||||||
|
(use montage/--small to save context). Disk is limited (~20 GB free): delete your copy's build dirs and session dirs at the end
|
||||||
|
(keep the session template _llamamod-template).
|
||||||
|
|
||||||
|
REPORT BACK: what the mod does (options), how it works (offsets/patches/data changes), what was verified in game (which
|
||||||
|
levels, screenshots paths – put 2–4 phone-friendly JPGs ≤1280px into ~/projects/llamamod/notes/img/mods/), known limits,
|
||||||
|
any framework changes. Also write notes/mod-<name>.md (English, concise).
|
||||||
|
|
||||||
|
## Extra rules for EDITOR phase-3 agents (several agents edit the editor in parallel)
|
||||||
|
- Right after rsync, make a pristine baseline of your start state: `cp -al <copy> <copy>.base` (hard links, cheap) –
|
||||||
|
actually use `rsync -a <copy>/ <copy>.base/` so later edits in the copy don't change it.
|
||||||
|
- Put your work in your OWN plugin package (class name ending in `EditorPlugin`, see notes/editor-ui.md) – no shell edits needed.
|
||||||
|
- Shared files (editor.model, editor.io, editor.render, editor.ui shell, format, levelbuild, framework): only minimal,
|
||||||
|
backwards-compatible additions. At copy-back, for every shared file you changed: if the main tree's version differs from
|
||||||
|
your baseline (another agent changed it meanwhile), DO NOT overwrite – re-apply your diff (`diff -u base copy | patch`)
|
||||||
|
onto the main tree's version and resolve conflicts by hand. New files of your own: copy directly.
|
||||||
|
- After copy-back: `./gradlew spotlessApply build` in ~/projects/llamamod must pass (retry later if another agent's
|
||||||
|
half-merged change breaks it; never edit their packages).
|
||||||
|
- Launch the editor: `llamamod --editor [file|t1l1m001]` (fat jar) on your UI display; screenshots via `import -display :N`.
|
||||||
|
|
||||||
|
## SIMPLIFY agents (overnight review waves)
|
||||||
|
- Goal: make your assigned area simpler and cleaner WITHOUT changing behaviour: remove duplication (extract shared helpers), dead/unused code, needless indirection and over-engineering, overly long methods (split where it clarifies), inconsistent naming, unnecessary comments (keep only non-obvious ones), stray debug code; tidy APIs used only internally (make private/package-private).
|
||||||
|
- Do NOT: rewrite architectures, change file formats or byte output, change UI behaviour/labels, change public APIs used by other areas (if needed, list it instead), touch areas assigned to other agents.
|
||||||
|
- Every golden/byte-exact test and all 300+ tests must stay green; add tests if you touch untested logic. Commit nothing.
|
||||||
|
- Report: what was simplified (with rough LOC before/after for your area), anything risky you deliberately left alone, and suggestions outside your area.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Coordinator rules for the 2026-11-07 open-points waves (read together with agent-brief.md)
|
||||||
|
|
||||||
|
- You are one of several agents working in parallel on ~/projects/llamamod. Read notes/dev/agent-brief.md fully,
|
||||||
|
INCLUDING the "EDITOR phase-3" merge rules – they apply to every agent in this wave, not only editor agents:
|
||||||
|
own copy under $CLAUDE_JOB_DIR/tmp/<agent>/llamamod, a pristine baseline copy (<copy>.base via rsync), and at
|
||||||
|
copy-back every shared file you changed is merged with `diff -u base copy | patch` onto the main tree's
|
||||||
|
current version (never blindly overwrite a file another agent may have changed meanwhile).
|
||||||
|
- Do NOT commit (the coordinator commits after each package). Do not touch git in the main tree.
|
||||||
|
- Build in your own copy: `JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew spotlessApply build fatJar`.
|
||||||
|
After copy-back run `JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew spotlessApply build` in the main tree
|
||||||
|
once; if it fails only because of another agent's half-merged change, wait a few minutes and retry.
|
||||||
|
- Every behaviour change needs a test (unit test where possible) and, for UI, a screenshot check on YOUR display;
|
||||||
|
for game-side changes an in-game check with tools/apptest.py (your display, session names prefixed with your name).
|
||||||
|
- Keep user-facing texts English, short and plain. Update the relevant notes/*.md and, if users see the change,
|
||||||
|
the manual (docs/manual) briefly.
|
||||||
|
- Out of scope for everyone: Windows hardware test, texture packs, German translation of the UI, a launcher
|
||||||
|
manual/help window, a OneDrive warning, git remotes.
|
||||||
|
- At the end delete your copy's build/, .gradle/, the .base copy and your apptest session dirs (disk is tight).
|
||||||
|
- REPORT (final message, English, concise): what you changed (files), how verified (tests, screenshots paths),
|
||||||
|
shared/framework files you touched, and a list "NEW ISSUES / NOT DONE" with anything you found but did not fix
|
||||||
|
(the coordinator turns those into follow-up tasks).
|
||||||
@@ -111,3 +111,5 @@ swallowed while the free camera is on.
|
|||||||
region's objects, or the backdrop only where the level has no geometry (open sea around islands).
|
region's objects, or the backdrop only where the level has no geometry (open sea around islands).
|
||||||
* A cutscene camera set via 0x57f8bc in the render function still wins over the free camera.
|
* A cutscene camera set via 0x57f8bc in the render function still wins over the free camera.
|
||||||
* No mouse look.
|
* No mouse look.
|
||||||
|
|
||||||
|
Hotkeys are checked together with the other mods' hotkeys (`mods/Hotkeys.conflicts`): applying fails with a clear message if two enabled hotkey options share a key.
|
||||||
@@ -88,3 +88,5 @@ to the real exe (no overlapping patches) and writes `build/tmp/practice-freecam.
|
|||||||
CheckPointPost, so it is expected to behave like the original ones.
|
CheckPointPost, so it is expected to behave like the original ones.
|
||||||
* Posts are placed at coins with floor below; a coin on a moving platform or behind a one-way
|
* Posts are placed at coins with floor below; a coin on a moving platform or behind a one-way
|
||||||
section can give a post the llama only reaches from one side.
|
section can give a post the llama only reaches from one side.
|
||||||
|
|
||||||
|
Hotkeys are checked together with the other mods' hotkeys (`mods/Hotkeys.conflicts`): applying fails with a clear message if two enabled hotkey options share a key.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.eliasmueller.llamamod.mods;
|
||||||
|
|
||||||
|
import com.eliasmueller.llamamod.platform.Config;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hotkey choices for cave code that polls GetAsyncKeyState. F6 (screenshot in the game's window
|
||||||
|
* procedure) and F10 (system key) are left out; the game's own controls are DirectInput keys.
|
||||||
|
*/
|
||||||
|
public final class Hotkeys {
|
||||||
|
private Hotkeys() {}
|
||||||
|
|
||||||
|
/** Option value → Windows virtual-key code. */
|
||||||
|
public static final Map<String, Integer> VK = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
int[] f = {1, 2, 3, 4, 5, 7, 8, 9, 11, 12};
|
||||||
|
for (int n : f) {
|
||||||
|
VK.put("F" + n, 0x6F + n);
|
||||||
|
}
|
||||||
|
VK.put("Insert", 0x2D);
|
||||||
|
VK.put("Delete", 0x2E);
|
||||||
|
VK.put("Home", 0x24);
|
||||||
|
VK.put("End", 0x23);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModOption option(String key, String label, String description, String def) {
|
||||||
|
ModOption.Choice[] c = VK.keySet().stream().map(k -> ModOption.of(k, k)).toArray(ModOption.Choice[]::new);
|
||||||
|
return ModOption.choice(key, label, description, def, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True for options created by {@link #option}. */
|
||||||
|
public static boolean isHotkey(ModOption o) {
|
||||||
|
return o.type() == ModOption.Type.CHOICE
|
||||||
|
&& o.choices().stream().map(ModOption.Choice::value).toList().equals(List.copyOf(VK.keySet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One message per key that more than one hotkey option of {@code mods} uses (within one mod or
|
||||||
|
* across mods); empty if all hotkeys are distinct.
|
||||||
|
*/
|
||||||
|
public static List<String> conflicts(List<Mod> mods, Config cfg) {
|
||||||
|
Map<String, List<String>> users = new LinkedHashMap<>();
|
||||||
|
for (Mod m : ModManager.sorted(mods)) {
|
||||||
|
Map<String, String> values = ModOptions.resolve(m, cfg);
|
||||||
|
for (ModOption o : m.options()) {
|
||||||
|
if (isHotkey(o)) {
|
||||||
|
users.computeIfAbsent(values.get(o.key()), k -> new ArrayList<>())
|
||||||
|
.add("\"" + o.label() + "\" (" + m.name() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
users.forEach((key, who) -> {
|
||||||
|
if (who.size() > 1) {
|
||||||
|
out.add(key + " is used by " + String.join(" and ", who));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int vk(String name) {
|
||||||
|
Integer v = VK.get(name);
|
||||||
|
if (v == null) {
|
||||||
|
throw new IllegalArgumentException("unknown hotkey " + name);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -347,6 +347,11 @@ public final class ModManager {
|
|||||||
if (gameRunning.getAsBoolean()) {
|
if (gameRunning.getAsBoolean()) {
|
||||||
throw new IOException("The game is running. Please quit it before applying or restoring mods.");
|
throw new IOException("The game is running. Please quit it before applying or restoring mods.");
|
||||||
}
|
}
|
||||||
|
List<String> clashes = Hotkeys.conflicts(mods, cfg);
|
||||||
|
if (!clashes.isEmpty()) {
|
||||||
|
throw new IOException("Two hotkeys share a key: " + String.join("; ", clashes)
|
||||||
|
+ ". Please choose different keys in the mod options.");
|
||||||
|
}
|
||||||
try (DataLock lock = DataLock.acquire(paths);
|
try (DataLock lock = DataLock.acquire(paths);
|
||||||
ApplyLog log = new ApplyLog(paths.logs().resolve("apply.log"))) {
|
ApplyLog log = new ApplyLog(paths.logs().resolve("apply.log"))) {
|
||||||
log.line("mods: " + sorted(mods).stream().map(Mod::id).toList());
|
log.line("mods: " + sorted(mods).stream().map(Mod::id).toList());
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.eliasmueller.llamamod.mods.freecam;
|
package com.eliasmueller.llamamod.mods.freecam;
|
||||||
|
|
||||||
|
import com.eliasmueller.llamamod.mods.Hotkeys;
|
||||||
import com.eliasmueller.llamamod.mods.Mod;
|
import com.eliasmueller.llamamod.mods.Mod;
|
||||||
import com.eliasmueller.llamamod.mods.ModContext;
|
import com.eliasmueller.llamamod.mods.ModContext;
|
||||||
import com.eliasmueller.llamamod.mods.ModOption;
|
import com.eliasmueller.llamamod.mods.ModOption;
|
||||||
import com.eliasmueller.llamamod.mods.practice.Hotkeys;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.eliasmueller.llamamod.mods.practice;
|
|
||||||
|
|
||||||
import com.eliasmueller.llamamod.mods.ModOption;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hotkey choices for cave code that polls GetAsyncKeyState. F6 (screenshot in the game's window
|
|
||||||
* procedure) and F10 (system key) are left out; the game's own controls are DirectInput keys.
|
|
||||||
*/
|
|
||||||
public final class Hotkeys {
|
|
||||||
private Hotkeys() {}
|
|
||||||
|
|
||||||
/** Option value → Windows virtual-key code. */
|
|
||||||
public static final Map<String, Integer> VK = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
int[] f = {1, 2, 3, 4, 5, 7, 8, 9, 11, 12};
|
|
||||||
for (int n : f) {
|
|
||||||
VK.put("F" + n, 0x6F + n);
|
|
||||||
}
|
|
||||||
VK.put("Insert", 0x2D);
|
|
||||||
VK.put("Delete", 0x2E);
|
|
||||||
VK.put("Home", 0x24);
|
|
||||||
VK.put("End", 0x23);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ModOption option(String key, String label, String description, String def) {
|
|
||||||
ModOption.Choice[] c = VK.keySet().stream().map(k -> ModOption.of(k, k)).toArray(ModOption.Choice[]::new);
|
|
||||||
return ModOption.choice(key, label, description, def, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int vk(String name) {
|
|
||||||
Integer v = VK.get(name);
|
|
||||||
if (v == null) {
|
|
||||||
throw new IllegalArgumentException("unknown hotkey " + name);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package com.eliasmueller.llamamod.mods.practice;
|
|||||||
|
|
||||||
import static com.eliasmueller.llamamod.mods.LevelWads.fileName;
|
import static com.eliasmueller.llamamod.mods.LevelWads.fileName;
|
||||||
|
|
||||||
|
import com.eliasmueller.llamamod.mods.Hotkeys;
|
||||||
import com.eliasmueller.llamamod.mods.LevelWads;
|
import com.eliasmueller.llamamod.mods.LevelWads;
|
||||||
import com.eliasmueller.llamamod.mods.Mod;
|
import com.eliasmueller.llamamod.mods.Mod;
|
||||||
import com.eliasmueller.llamamod.mods.ModContext;
|
import com.eliasmueller.llamamod.mods.ModContext;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.eliasmueller.llamamod.mods;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.eliasmueller.llamamod.mods.freecam.FreeCamMod;
|
||||||
|
import com.eliasmueller.llamamod.mods.practice.PracticeMod;
|
||||||
|
import com.eliasmueller.llamamod.platform.Config;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class HotkeysTest {
|
||||||
|
private final Mod practice = new PracticeMod();
|
||||||
|
private final Mod freecam = new FreeCamMod();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultsDoNotClash() {
|
||||||
|
assertTrue(Hotkeys.conflicts(List.of(practice, freecam), new Config()).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameKeyAcrossModsIsReported() {
|
||||||
|
Config cfg = new Config();
|
||||||
|
ModOptions.set(cfg, freecam, "camKey", "F5");
|
||||||
|
List<String> c = Hotkeys.conflicts(List.of(practice, freecam), cfg);
|
||||||
|
assertEquals(1, c.size());
|
||||||
|
assertTrue(c.get(0).startsWith("F5 is used by"), c.get(0));
|
||||||
|
assertTrue(Hotkeys.conflicts(List.of(freecam), cfg).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sameKeyWithinOneModIsReported() {
|
||||||
|
Config cfg = new Config();
|
||||||
|
ModOptions.set(cfg, practice, "loadKey", "F5");
|
||||||
|
assertEquals(1, Hotkeys.conflicts(List.of(practice), cfg).size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onlyHotkeyOptionsCount() {
|
||||||
|
assertTrue(practice.options().stream().anyMatch(Hotkeys::isHotkey));
|
||||||
|
assertTrue(practice.options().stream()
|
||||||
|
.filter(o -> !o.key().endsWith("Key"))
|
||||||
|
.noneMatch(Hotkeys::isHotkey));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user