Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,19 @@ public KeyBindingControl(ChangePipeline pipeline) {
}

/**
* Fires when the keys {@code options.txt} assigns changed, as read after Companion changed them or when the user came
* back to Companion, such as from the game's controls screen; another option written, such as the volume, tells nobody.
* Fires when the keys {@code options.txt} assigns changed, as read after Companion changed them, after the game told
* it saved changed keys, or when the user came back to Companion; another option written, such as the volume, tells
* nobody.
*/
public Signal assignmentsChanged() {
return this.assignments.changed();
}

/** The game saved changed keys in {@code options.txt}, as after its controls screen closed: they are read again. */
public void saved() {
this.assignments.refresh();
}

/** The keys {@code options.txt} assigns, as read last; read now where none were read yet. Blocking then. */
public Map<String, KeyBindings.Assignment> assignments() throws IOException {
return this.assignments.value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.github.minecraft_ta.totalDebugCompanion.change.WriteQueue;
import com.github.minecraft_ta.totalDebugCompanion.catalog.WorldReading;
import com.github.minecraft_ta.totaldebug.protocol.scnet.RelayFailedMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.KeyAssignmentsMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.PlayingMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.DatapacksMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.PackStackMessage;
Expand Down Expand Up @@ -135,6 +136,7 @@ public Runnable listen(MessageRoutes routes) {
routes.on(PackStackMessage.class, message -> this.packs.named(message.payload())),
routes.on(DatapacksMessage.class, message -> this.packs.datapacks(message.world(), message.payload(), message.refusal())),
routes.on(PlayingMessage.class, message -> this.location.playing(message.payload())),
routes.on(KeyAssignmentsMessage.class, message -> this.keyBindings.saved()),
// The refused message and its correlation name the request together.
routes.on(RelayFailedMessage.class, message -> {
switch (message.messageId()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
import com.github.minecraft_ta.totalDebugCompanion.session.MessageRoutes;
import com.github.minecraft_ta.totaldebug.protocol.message.ClientPacksPayload;
import com.github.minecraft_ta.totaldebug.protocol.message.PackStackPayload;
import com.github.minecraft_ta.totaldebug.protocol.scnet.KeyAssignmentsMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.PackStackMessage;
import com.github.tth05.scnet.message.AbstractMessage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -41,6 +45,32 @@ void aProjectTakesItsMessagesUntilAnotherBecomesCurrent() throws Exception {
}
}

@Test
void theGameSavingChangedKeysHasTheKeysReadAgain() throws Exception {
Routes routes = new Routes();
ProjectScope scope = ProjectScope.open(new Object(), CompanionProfile.forGame(this.directory));
try {
Path options = scope.profile().workspaceDirectory().resolve("options.txt");
Files.writeString(options, "key_key.jump:key.keyboard.space\n");
Runnable stop = scope.listen(routes);
AtomicInteger told = new AtomicInteger();
scope.keyBindings().assignmentsChanged().subscribe(told::incrementAndGet);
assertEquals("key.keyboard.space", scope.keyBindings().assignments().get("key.jump").encode());

// The player rebinds jump in the game's controls screen, which saves options.txt as it closes.
Files.writeString(options, "key_key.jump:key.keyboard.g\n");
routes.deliver(new KeyAssignmentsMessage());
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (told.get() == 0 && System.nanoTime() < deadline) Thread.sleep(10);
assertEquals(1, told.get(), "the Key bindings page and the change labels hear of it");
assertEquals("key.keyboard.g", scope.keyBindings().assignments().get("key.jump").encode());
stop.run();
} finally {
scope.retire();
scope.close();
}
}

private static PackStackPayload stack(String pack) {
return new PackStackPayload(34, List.of(new PackStackPayload.Pack(pack, pack, "")));
}
Expand Down
4 changes: 2 additions & 2 deletions docs/SYSTEMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Nothing watches the files others write, apart from one case. Who wrote a file de

| Who wrote it | How Companion learns of it |
|---|---|
| The connected game | Its messages: packs, `PLAYING`, the answers to changes and reloads (section 6) |
| The connected game | Its messages: packs, `PLAYING`, the answers to changes and reloads, and that it saved changed key bindings (section 6) |
| Companion | The owner reads again after the write and fires if the value differs (section 5) |
| Another program, or a game without a connection | What shows the file reads it when it is shown, and again when the user comes back to Companion from another program (`WindowFocus.returned()`: a window of Companion taking the focus after Companion lost it to another program, which the main window tells of; Companion's first window opening is no return) |
| A program Companion opened a file in | `FileWatch`, which takes its saves at once (below) |
Expand Down Expand Up @@ -115,7 +115,7 @@ Its whole contract, which `FileReading`'s documentation states the same way:

**Adopting a file is a write, not a reading.** A texture opened in another program from Companion is taken into the pack as that program saves it, while the user stays in the editor and looks at the game. This is the one watch: `FileWatch` tells `ExternalEdits` that a followed file was written, on its one thread; `ExternalEdits` keeps the file's baseline, waits until writes stopped, checks that the image is complete, and adopts it on the project's write queue through the pipeline. Before it records a change or asks for a reload, the adoption compares the saved content with what the pack already holds, by hash, and does nothing when they are the same: a notification of content Companion itself wrote, or a second notification of one save, must not record or reload again. Deduplication happens before side effects, in the owner, never after them in a page. A file whose folder cannot be watched is not opened, and the user is told why. Every opening watches the file anew, so one whose folder was removed and made again is followed again.

**Why nothing else is watched.** Decided on 2026-10-01, after #114 to #117 moved every watch onto one watcher. Watching folders that others own needed watches of ancestors for folders not there yet, real paths for links, retries, pauses so that Windows can rename or delete a watched folder (the game's Delete World), and settle times for files written in parts; most review findings of those PRs were about these. Reading when shown and when the user comes back answers the same question with none of them. A change that must show while Companion keeps the focus, as a key rebound in the game while Companion is visible beside it, comes as a game message, not as a watch.
**Why nothing else is watched.** Decided on 2026-10-01, after #114 to #117 moved every watch onto one watcher. Watching folders that others own needed watches of ancestors for folders not there yet, real paths for links, retries, pauses so that Windows can rename or delete a watched folder (the game's Delete World), and settle times for files written in parts; most review findings of those PRs were about these. Reading when shown and when the user comes back answers the same question with none of them. A change that must show while Companion keeps the focus comes as a game message, not as a watch: a key rebound in the game while Companion is visible beside it comes as `KEY_ASSIGNMENTS`. When any screen closes, the game compares its key bindings with those when a screen closed before, and where they differ, tells Companion once the screen saved `options.txt`, which it does as the screen is removed, after NeoForge's `ScreenEvent.Closing`. The message carries nothing: Companion reads `options.txt` again, which stays the one place it reads keys from.

## 3. Pages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public final class TotalDebugClient {
private volatile boolean snapshotRequested;
/** What the game last told Companion it plays, whose world every world-bound request must name. Client thread writes. */
private volatile PlayingPayload playing = new PlayingPayload.Menu();
/** The key bindings when a screen closed last, as {@code options.txt} saves them; null before. Client thread only. */
private Map<String, String> keys;

private TotalDebugClient(Path gameDirectory) {
Path totalDebugDirectory = gameDirectory
Expand Down Expand Up @@ -197,6 +199,19 @@ public void packsChanged() {
this.packStacks.publish();
}

/**
* A screen closes. Where the key bindings differ from those when a screen closed before, as after the controls screen,
* Companion is told once the screen saved {@code options.txt}: the game saves it as the screen is removed, after this
* event. Client thread only.
*/
public void screenClosing() {
Map<String, String> assigned = KeyBindingEdits.assigned();
if (assigned.equals(this.keys)) return;
this.keys = assigned;
// Queued rather than run now, as Minecraft.execute would on this thread.
Minecraft.getInstance().tell(this.companionApp::sendKeyAssignments);
}

/** The player joined a singleplayer world or a server. Client thread only. */
public void joined() {
play(Playing.joined());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import net.minecraft.client.Minecraft;
import net.neoforged.neoforge.client.settings.KeyModifier;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
Expand All @@ -19,7 +21,17 @@ public final class KeyBindingEdits implements ChangeTable.Category {

@Override
public String read(String name) {
KeyMapping mapping = mapping(name);
return value(mapping(name));
}

/** Every binding's key, as {@code options.txt} saves it, by the binding's name. */
public static Map<String, String> assigned() {
Map<String, String> assigned = new HashMap<>();
for (KeyMapping mapping : Minecraft.getInstance().options.keyMappings) assigned.put(mapping.getName(), value(mapping));
return assigned;
}

private static String value(KeyMapping mapping) {
KeyModifier modifier = mapping.getKeyModifier();
return mapping.getKey().getName() + (modifier == KeyModifier.NONE ? "" : ":" + modifier.name());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.github.minecraft_ta.totaldebug.protocol.scnet.ChangeMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ChangeResultMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.PackStackMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.KeyAssignmentsMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.PlayingMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ReloadMessage;
import com.github.minecraft_ta.totaldebug.protocol.scnet.ReloadResultMessage;
Expand Down Expand Up @@ -306,6 +307,11 @@ public void sendPlaying(PlayingMessage message) {
send(message);
}

/** Tells Companion that the game saved changed key bindings in {@code options.txt}. */
public void sendKeyAssignments() {
send(new KeyAssignmentsMessage());
}

public void setProgressListener(Consumer<CompanionStartupProgress> listener) {
this.progressListener = Objects.requireNonNull(listener, "listener");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) {
}

/**
* The Resource Packs screen rescans the pack folders while it is open. Closing it without changing the selection
* starts no resource reload, so the packs it found are told here. It selected the packs before it closes.
* A screen that changed key bindings, as the game's or a mod's controls screen, has Companion told. The Resource
* Packs screen rescans the pack folders while it is open; closing it without changing the selection starts no
* resource reload, so the packs it found are told here. It selected the packs before it closes.
*/
@SubscribeEvent
static void onScreenClosing(ScreenEvent.Closing event) {
TotalDebugClient.current().ifPresent(TotalDebugClient::screenClosing);
if (!(event.getScreen() instanceof PackSelectionScreen)) return;
ResourceReloads.selected();
TotalDebugClient.current().ifPresent(TotalDebugClient::packsChanged);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.github.minecraft_ta.totaldebug.protocol;

public final class CompanionProtocol {
public static final int VERSION = 41;
public static final int VERSION = 42;

public static final short READY = 1;
// 2 was the game opening a class, which the /decompile command used.
Expand Down Expand Up @@ -37,6 +37,7 @@ public final class CompanionProtocol {
public static final short CHANGE_RESULT = 49;
public static final short DATAPACKS = 50;
public static final short DATAPACKS_REQUEST = 51;
public static final short KEY_ASSIGNMENTS = 52;

private CompanionProtocol() {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.github.minecraft_ta.totaldebug.protocol.scnet;

import com.github.tth05.scnet.message.AbstractMessage;
import com.github.tth05.scnet.util.ByteBufferInputStream;
import com.github.tth05.scnet.util.ByteBufferOutputStream;

/**
* The game tells Companion that its key bindings changed and that {@code options.txt} holds them now, as after the
* player rebound a key in the game's controls screen. Companion reads the file again; the message carries nothing, so
* the file stays the one place the keys are read from.
*/
public final class KeyAssignmentsMessage extends AbstractMessage {
@Override
public void read(ByteBufferInputStream input) {
}

@Override
public void write(ByteBufferOutputStream output) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static void registerMod(IMessageProcessor processor) {
processor.registerOutgoing(CompanionProtocol.PREPARED_FILE, PreparedFileMessage.class);
processor.registerIncoming(CompanionProtocol.CHANGE, ChangeMessage.class, ChangeMessage::new);
processor.registerOutgoing(CompanionProtocol.CHANGE_RESULT, ChangeResultMessage.class);
processor.registerOutgoing(CompanionProtocol.KEY_ASSIGNMENTS, KeyAssignmentsMessage.class);
}

public static void registerCompanion(IMessageProcessor processor) {
Expand All @@ -52,5 +53,6 @@ public static void registerCompanion(IMessageProcessor processor) {
processor.registerIncoming(CompanionProtocol.PREPARED_FILE, PreparedFileMessage.class, PreparedFileMessage::new);
processor.registerOutgoing(CompanionProtocol.CHANGE, ChangeMessage.class);
processor.registerIncoming(CompanionProtocol.CHANGE_RESULT, ChangeResultMessage.class, ChangeResultMessage::new);
processor.registerIncoming(CompanionProtocol.KEY_ASSIGNMENTS, KeyAssignmentsMessage.class, KeyAssignmentsMessage::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
public final class GoldenMessages {
public static final String RUN_SCRIPT = "0000000700000001580000000100000001580000000301020300000009696e76656e746f72790100000009504f53545f5449434b000000000000000000000000";
public static final String STOP_SCRIPT = "00000007";
public static final String CLIENT_HELLO = "0000002900000003616263000000017000000001640000000177";
public static final String SERVER_HELLO = "000000290100000000";
public static final String CLIENT_HELLO = "0000002a00000003616263000000017000000001640000000177";
public static final String SERVER_HELLO = "0000002a0100000000";
public static final String PREPARED_FILE = "00000001000000010000000269640000000466696c6500000000";
public static final String DEBUG_TARGET = "0000000269640000000467616d6501000000000000002a";

Expand Down