This commit is contained in:
iceBear67
2026-08-07 18:10:38 +00:00
commit 014b18f1ba
232 changed files with 16847 additions and 0 deletions
@@ -0,0 +1,23 @@
package dev.photosync.platform;
import dev.photosync.mcapi.Clipboard;
import net.minecraft.client.Minecraft;
/**
* The system clipboard, via the keyboard handler that already owns GLFW's.
*
* <p>Going through Minecraft rather than AWT matters on macOS, where touching
* {@code java.awt} from the render thread of a GLFW application is a hang.
*/
public final class ClipboardAdapter implements Clipboard {
@Override
public String read() {
return Minecraft.getInstance().keyboardHandler.getClipboard();
}
@Override
public void write(String text) {
Minecraft.getInstance().keyboardHandler.setClipboard(text);
}
}
@@ -0,0 +1,28 @@
package dev.photosync.platform;
import dev.photosync.mcapi.ClientBridge;
import dev.photosync.platform.impl.CaptureAdapter;
import dev.photosync.platform.impl.GameAdapter;
import dev.photosync.platform.impl.ScreenAdapter;
import dev.photosync.platform.impl.TextureAdapter;
import lombok.Getter;
import lombok.experimental.Accessors;
/**
* Assembles this bucket's adapters into the one object the rest of the mod sees.
*
* <p>Built once, at client startup, before Minecraft has finished constructing
* itself -- so nothing here may touch the game. Each adapter resolves what it
* needs when it is first used instead.
*/
@Getter
@Accessors(fluent = true)
public final class MinecraftBridge implements ClientBridge {
private final GameAdapter game = new GameAdapter();
private final TextAdapter text = new TextAdapter();
private final ClipboardAdapter clipboard = new ClipboardAdapter();
private final TextureAdapter textures = new TextureAdapter();
private final ScreenAdapter screens = new ScreenAdapter();
private final CaptureAdapter screenshots = new CaptureAdapter();
}
@@ -0,0 +1,65 @@
package dev.photosync.platform;
import dev.photosync.client.PhotoSyncClient;
import dev.photosync.platform.impl.OpenKey;
import lombok.extern.slf4j.Slf4j;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import java.util.Optional;
/**
* Fabric's way in, and the mixins' way back.
*
* <p>Three lines of wiring: build the client against this bucket's bridge, tick
* it, and close it when the game is shutting down. Everything else the mod does
* is reached from those.
*
* <p>The static field is the same concession {@code ScreenshotBus} and
* {@code QuitGuard} make, for the same reason -- a mixin is woven into a class
* nobody constructs, so it cannot be handed a collaborator. Those two cover the
* capture and quit hooks between them; this covers the third, the HUD, which
* needs the client itself because it draws through it. Keeping the field here
* rather than in each bucket's mixin package means there is exactly one of them.
*/
@Slf4j
public final class PhotoSyncMod implements ClientModInitializer {
private static volatile PhotoSyncClient client;
@Override
public void onInitializeClient() {
OpenKey openKey = OpenKey.register();
PhotoSyncClient started = new PhotoSyncClient(new MinecraftBridge()).start();
client = started;
ClientTickEvents.END_CLIENT_TICK.register(game -> {
started.tick();
// Drained in a loop: holding the key over a lag spike queues several
// presses, and opening the screen three times is not what was meant.
boolean pressed = false;
while (openKey.wasPressed()) {
pressed = true;
}
if (pressed) {
started.openScreen();
}
});
ClientLifecycleEvents.CLIENT_STOPPING.register(game -> {
client = null;
started.close();
});
}
/**
* The running client, for the HUD mixin.
*
* <p>Empty before {@code onInitializeClient} finishes and again once the
* game is stopping, both of which are frames the HUD can still be drawn in.
*/
public static Optional<PhotoSyncClient> client() {
return Optional.ofNullable(client);
}
}
@@ -0,0 +1,26 @@
package dev.photosync.platform;
import dev.photosync.mcapi.Translator;
import net.minecraft.client.resources.language.I18n;
import net.minecraft.locale.Language;
/**
* Translations, from the language the player actually selected.
*
* <p>{@code I18n.get} falls back to the key when there is no entry, which is
* what {@link Translator} promises. The existence check goes to {@link Language}
* rather than {@code I18n.exists} because that method was dropped in 26.1 while
* {@code Language.has} has been there throughout.
*/
public final class TextAdapter implements Translator {
@Override
public String get(String key, Object... arguments) {
return I18n.get(key, arguments);
}
@Override
public boolean has(String key) {
return Language.getInstance().has(key);
}
}
@@ -0,0 +1,15 @@
/**
* The half of the Minecraft adapter that has not changed since 1.20.
*
* <p>This is a source root, not a module: it imports {@code net.minecraft}, so
* it is compiled once per compatibility bucket against that bucket's Minecraft
* rather than published as a jar. The root build adds it to every
* {@code :platform:*} project.
*
* <p>A class earns its place here by being <em>textually identical</em> on all
* nine buckets -- verified by the fact that nine compilations accept it. The
* moment a version needs a different body, the class moves down into
* {@link dev.photosync.platform.impl}, which every bucket supplies for itself.
* That is the whole rule; there is no third category.
*/
package dev.photosync.platform;
@@ -0,0 +1,29 @@
{
"schemaVersion": 1,
"id": "${mod_id}",
"version": "${mod_version}",
"name": "${mod_name}",
"description": "${mod_description}",
"license": "${mod_license}",
"environment": "client",
"contact": {
"sources": "${mod_sources}"
},
"entrypoints": {
"client": [
"dev.photosync.platform.PhotoSyncMod"
]
},
"mixins": [
{
"config": "${mod_id}.mixins.json",
"environment": "client"
}
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric-api": "*",
"minecraft": "${minecraft_range}",
"java": ">=${java_version}"
}
}
@@ -0,0 +1,14 @@
{
"required": true,
"minVersion": "0.8",
"package": "dev.photosync.platform.mixin",
"compatibilityLevel": "${mixin_compat}",
"client": [
"CaptureMixin",
"HudMixin",
"QuitMixin"
],
"injectors": {
"defaultRequire": 1
}
}