Skip to content
Open
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
275 changes: 275 additions & 0 deletions docs/internal/retrospective-audit/AUDIT-014.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import dev.hylfrd.farmhelper.client.platform.ClientTickAdapter;
import dev.hylfrd.farmhelper.client.platform.ClientCommandScreenCloseGuard;
import dev.hylfrd.farmhelper.client.platform.ClientScreenLifecycleAdapter;
import dev.hylfrd.farmhelper.client.runtime.FarmHelperClientRuntime;
import dev.hylfrd.farmhelper.client.ui.command.FarmHelperCommands;
import dev.hylfrd.farmhelper.client.ui.settings.FarmHelperSettingsController;
import dev.hylfrd.farmhelper.platform.FarmHelper;
import net.fabricmc.api.ClientModInitializer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.client.Minecraft;

public final class FarmHelperClient implements ClientModInitializer {
private static volatile FarmHelperClientRuntime activeRuntime;
private static volatile ClientScreenLifecycleAdapter activeScreenLifecycle;

@Override
public void onInitializeClient() {
Expand All @@ -20,10 +23,18 @@ public void onInitializeClient() {
ClientCommandScreenCloseGuard commandScreenClose = new ClientCommandScreenCloseGuard();
FarmHelperSettingsController settings = FarmHelperSettingsController.register(runtime);
FarmHelperCommands.register(runtime, settings, commandScreenClose);
ClientTickAdapter.register(runtime, commandScreenClose);
activeScreenLifecycle = ClientTickAdapter.register(runtime, commandScreenClose);
FarmHelper.LOGGER.info("FarmHelper client initialized.");
}

/** Receives the real Minecraft#setScreen tail boundary from the client Mixin. */
public static void recordScreenChange(Minecraft client) {
ClientScreenLifecycleAdapter screenLifecycle = activeScreenLifecycle;
if (screenLifecycle != null) {
screenLifecycle.observeSetScreen(client);
}
}

public static void recordServerTimePacket() {
FarmHelperClientRuntime runtime = activeRuntime;
if (runtime != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void observeScreen(
clearExpectedClose();
}

lifecycle.observeScreen(screen);
lifecycle.observeScreen(screen, screen.isPresent() && !chatScreen);
if (screen.isPresent() && chatScreen && currentScreen != null) {
observedChatScreen = currentScreen;
observedChatScreenIdentity = screen.get().identity();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dev.hylfrd.farmhelper.client.platform;

import dev.hylfrd.farmhelper.client.runtime.FarmHelperClientRuntime;
import dev.hylfrd.farmhelper.platform.FarmHelper;
import dev.hylfrd.farmhelper.runtime.snapshot.ClientSnapshot;
import dev.hylfrd.farmhelper.runtime.snapshot.Observation;
import dev.hylfrd.farmhelper.runtime.snapshot.ScreenSnapshot;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.gui.screens.Screen;

import java.util.Objects;

/**
* Shares the screen lifecycle state between the real Minecraft#setScreen boundary and tick
* snapshots. Non-chat openings are terminal; chat closes armed by a command remain exempt.
*/
public final class ClientScreenLifecycleAdapter {
private final FarmHelperClientRuntime runtime;
private final ClientCommandScreenCloseGuard commandScreenClose;
private final ClientSnapshotCapture snapshots;

ClientScreenLifecycleAdapter(
FarmHelperClientRuntime runtime,
ClientCommandScreenCloseGuard commandScreenClose,
ClientSnapshotCapture snapshots
) {
this.runtime = Objects.requireNonNull(runtime, "runtime");
this.commandScreenClose = Objects.requireNonNull(commandScreenClose, "commandScreenClose");
this.snapshots = Objects.requireNonNull(snapshots, "snapshots");
}

/** Receives the client-thread tail of Minecraft#setScreen. */
public void observeSetScreen(Minecraft client) {
Objects.requireNonNull(client, "client");
try {
if (!client.isSameThread()) {
client.execute(() -> observeSetScreen(client));
return;
}
observeBoundary(snapshots.captureScreen(client.screen), client.screen,
client.screen instanceof ChatScreen);
} catch (RuntimeException | Error failure) {
ClientTickAdapter.fail(runtime, commandScreenClose);
FarmHelper.LOGGER.error("FarmHelper screen lifecycle boundary failed", failure);
}
}

void observeSnapshot(ClientSnapshot snapshot, Screen currentScreen) {
Objects.requireNonNull(snapshot, "snapshot");
observeBoundary(snapshot.screen(), currentScreen, currentScreen instanceof ChatScreen);
}

void observeBoundary(
Observation<ScreenSnapshot> screen,
Object currentScreen,
boolean chatScreen
) {
Objects.requireNonNull(screen, "screen");
runtime.observeMacroScreen(screen);
commandScreenClose.observeScreen(
screen, currentScreen, chatScreen,
runtime.lifecycle(), runtime.ownershipGeneration());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ public ClientSnapshot capture(Minecraft client, long worldEpoch) {
? Observation.present(playerSnapshot(client.player))
: Observation.absent();
Observation<ConnectionSnapshot> connection = connection(client, worldReady);
Observation<ScreenSnapshot> screen = screen(client.screen);
Observation<ScreenSnapshot> screen = captureScreen(client.screen);
return new ClientSnapshot(player, world, connection, screen);
}

private Observation<ScreenSnapshot> screen(Screen current) {
Observation<ScreenSnapshot> captureScreen(Screen current) {
if (current == null) {
observeScreenIdentity(null, null);
return Observation.absent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.network.chat.Component;

import java.util.Objects;
Expand All @@ -46,6 +45,7 @@ public final class ClientTickAdapter implements ClientTickPipeline.Actions {
private final ClientGameTextSource gameText;
private final ClientTickPipeline pipeline;
private final ClientLifecycleSequencer clientLifecycle;
private final ClientScreenLifecycleAdapter screenLifecycle;
private final ClientCommandScreenCloseGuard commandScreenClose;

ClientTickAdapter(
Expand All @@ -54,25 +54,30 @@ public final class ClientTickAdapter implements ClientTickPipeline.Actions {
ClientSnapshotCapture snapshots,
ClientGameTextSource gameText,
ClientTickPipeline pipeline,
ClientScreenLifecycleAdapter screenLifecycle,
ClientCommandScreenCloseGuard commandScreenClose) {
this.client = Objects.requireNonNull(client, "client");
this.runtime = Objects.requireNonNull(runtime, "runtime");
this.snapshots = Objects.requireNonNull(snapshots, "snapshots");
this.gameText = Objects.requireNonNull(gameText, "gameText");
this.pipeline = Objects.requireNonNull(pipeline, "pipeline");
this.screenLifecycle = Objects.requireNonNull(screenLifecycle, "screenLifecycle");
this.commandScreenClose = Objects.requireNonNull(commandScreenClose, "commandScreenClose");
clientLifecycle = new ClientLifecycleSequencer(runtime, gameText, commandScreenClose::clear);
}

public static void register(
public static ClientScreenLifecycleAdapter register(
FarmHelperClientRuntime runtime,
ClientCommandScreenCloseGuard commandScreenClose
) {
Minecraft client = Minecraft.getInstance();
ClientSnapshotCapture snapshots = new ClientSnapshotCapture();
ClientScreenLifecycleAdapter screenLifecycle = new ClientScreenLifecycleAdapter(
runtime, commandScreenClose, snapshots);
ClientTickAdapter adapter = new ClientTickAdapter(
client, runtime, new ClientSnapshotCapture(),
client, runtime, snapshots,
new MinecraftGameTextSnapshotSource(client), new ClientTickPipeline(),
commandScreenClose);
screenLifecycle, commandScreenClose);

ClientTickEvents.END_CLIENT_TICK.register(ignored -> adapter.tick());
ClientLevelEvents.AFTER_CLIENT_LEVEL_CHANGE.register(
Expand All @@ -88,6 +93,7 @@ client, runtime, new ClientSnapshotCapture(),
});
ClientLifecycleEvents.CLIENT_STOPPING.register(
ignored -> adapter.dispatch(adapter.clientLifecycle::clientStopping));
return screenLifecycle;
}

/** Runs the fixed vanilla system-packet boundary used by the GAME callback. */
Expand Down Expand Up @@ -196,11 +202,8 @@ public ClientSnapshot captureClientSnapshot() {

@Override
public void observeSnapshotLifecycle(ClientSnapshot snapshot) {
runtime.observeMacroScreen(snapshot.screen());
screenLifecycle.observeSnapshot(snapshot, client.screen);
clientLifecycle.observeConnection(snapshot.connection());
commandScreenClose.observeScreen(
snapshot.screen(), client.screen, client.screen instanceof ChatScreen,
runtime.lifecycle(), runtime.ownershipGeneration());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.hylfrd.farmhelper.client.platform.mixin;

import dev.hylfrd.farmhelper.client.FarmHelperClient;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

/** Captures the authoritative post-mutation Minecraft#setScreen lifecycle boundary. */
@Mixin(Minecraft.class)
abstract class MinecraftSetScreenMixin {
@Inject(method = "setScreen(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("TAIL"))
private void farmhelper$recordScreenChange(Screen screen, CallbackInfo callbackInfo) {
FarmHelperClient.recordScreenChange((Minecraft) (Object) this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ private void cancelMacro(ClientCancellationReason reason) {
}
MacroTerminalReason terminal = switch (reason) {
case MANUAL_STOP -> MacroTerminalReason.MANUAL_STOP;
case SCREEN_OPEN -> MacroTerminalReason.MANUAL_STOP;
case WORLD_LOAD, WORLD_UNLOAD -> disconnectLatched
? MacroTerminalReason.DISCONNECT
: MacroTerminalReason.WORLD_CHANGE;
Expand All @@ -617,6 +618,7 @@ private void cancelMacro(ClientCancellationReason reason) {

private void cancelDesync(ClientCancellationReason reason) {
switch (reason) {
case SCREEN_OPEN -> desyncChecker.stop();
case SCREEN_CHANGED -> desyncChecker.pause();
case WORLD_LOAD, WORLD_UNLOAD -> desyncChecker.worldChanged(lifecycle.worldEpoch());
case DISCONNECT, CONNECTION_UNAVAILABLE -> desyncChecker.connectionLost();
Expand All @@ -627,6 +629,7 @@ private void cancelDesync(ClientCancellationReason reason) {
private void cancelNavigation(ClientCancellationReason reason) {
NavigationCancellationReason terminal = switch (reason) {
case MANUAL_STOP -> NavigationCancellationReason.STOPPED;
case SCREEN_OPEN -> NavigationCancellationReason.SCREEN_CHANGED;
case SCREEN_CHANGED -> NavigationCancellationReason.SCREEN_CHANGED;
case WORLD_LOAD, WORLD_UNLOAD -> disconnectLatched
? NavigationCancellationReason.DISCONNECTED
Expand All @@ -640,7 +643,8 @@ private void cancelNavigation(ClientCancellationReason reason) {
}

private void resetServerHeartbeat(ClientCancellationReason reason) {
if (reason == ClientCancellationReason.SCREEN_CHANGED) {
if (reason == ClientCancellationReason.SCREEN_OPEN
|| reason == ClientCancellationReason.SCREEN_CHANGED) {
return;
}
serverHeartbeatJoined = false;
Expand Down Expand Up @@ -670,6 +674,7 @@ private void cancelOwnership(ClientCancellationReason reason) {
private static InventoryCancelReason inventoryReason(ClientCancellationReason reason) {
return switch (reason) {
case MANUAL_STOP -> InventoryCancelReason.REQUESTED;
case SCREEN_OPEN -> InventoryCancelReason.SCREEN_OPEN;
case SCREEN_CHANGED -> InventoryCancelReason.SCREEN_CHANGED;
case WORLD_LOAD, WORLD_UNLOAD -> InventoryCancelReason.WORLD_CHANGED;
case DISCONNECT, CONNECTION_UNAVAILABLE -> InventoryCancelReason.DISCONNECTED;
Expand All @@ -681,6 +686,7 @@ private static InventoryCancelReason inventoryReason(ClientCancellationReason re
private static RotationCancelReason rotationReason(ClientCancellationReason reason) {
return switch (reason) {
case MANUAL_STOP -> RotationCancelReason.STOPPED;
case SCREEN_OPEN -> RotationCancelReason.SCREEN_CHANGED;
case SCREEN_CHANGED -> RotationCancelReason.SCREEN_CHANGED;
case WORLD_LOAD, WORLD_UNLOAD -> RotationCancelReason.WORLD_CHANGED;
case DISCONNECT, CONNECTION_UNAVAILABLE -> RotationCancelReason.DISCONNECTED;
Expand All @@ -692,6 +698,7 @@ private static RotationCancelReason rotationReason(ClientCancellationReason reas
private static ReleaseReason inputReason(ClientCancellationReason reason) {
return switch (reason) {
case MANUAL_STOP -> ReleaseReason.STOP;
case SCREEN_OPEN -> ReleaseReason.SCREEN;
case SCREEN_CHANGED -> ReleaseReason.SCREEN;
case WORLD_LOAD, WORLD_UNLOAD -> ReleaseReason.WORLD_CHANGE;
case DISCONNECT, CONNECTION_UNAVAILABLE -> ReleaseReason.DISCONNECT;
Expand Down
1 change: 1 addition & 0 deletions src/client/resources/farmhelper.client.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_25",
"client": [
"ClientPacketListenerMixin",
"MinecraftSetScreenMixin",
"MultiPlayerGameModeMixin",
"PlayerTabOverlayAccessor"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum ClientCancellationReason {
WORLD_UNLOAD,
DISCONNECT,
CONNECTION_UNAVAILABLE,
SCREEN_OPEN,
SCREEN_CHANGED,
EXCEPTION,
CLIENT_STOP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ public void failed() {
}

public void observeScreen(Observation<ScreenSnapshot> screen) {
observeScreen(screen, false);
}

/**
* Observes one screen boundary. Non-chat present-screen transitions use the terminal
* SCREEN_OPEN boundary; all other transitions retain the ordinary SCREEN_CHANGED boundary.
*/
public void observeScreen(
Observation<ScreenSnapshot> screen,
boolean nonChatScreenOpen
) {
Objects.requireNonNull(screen, "screen");
ScreenIdentity identity = ScreenIdentity.from(screen);
if (!screenObserved) {
Expand All @@ -71,7 +82,9 @@ public void observeScreen(Observation<ScreenSnapshot> screen) {
}
if (!this.screen.equals(identity)) {
this.screen = identity;
cancellation.accept(ClientCancellationReason.SCREEN_CHANGED);
cancellation.accept(nonChatScreenOpen && screen.isPresent()
? ClientCancellationReason.SCREEN_OPEN
: ClientCancellationReason.SCREEN_CHANGED);
}
}

Expand Down
Loading
Loading