Multiloader scaffold: Architectury common+fabric+forge+neoforge (1.21.1)

Convert the single-loader NeoForge project into an Architectury multiloader:
- root/settings/subproject build.gradle, Gradle pinned to 8.10.2 (Loom needs 8.x)
- common: generic SkinsgCommands<S> + SkinsgClient bootstrap
- per-loader entrypoints (Fabric ClientModInitializer, Forge/NeoForge @Mod) + metadata
- migrate-to-multiloader.sh moves the loader/version-agnostic core into common/
- MULTILOADER.md: build/toolchain/proxy + 1.20.1 switch incl. rewritten cape mixin

main (working NeoForge 1.21.1) untouched.
This commit is contained in:
f1den
2026-07-05 18:10:53 +03:00
parent 6e3de2716c
commit c11a278050
18 changed files with 684 additions and 188 deletions
+10
View File
@@ -0,0 +1,10 @@
architectury {
common(rootProject.enabled_platforms.split(","))
}
dependencies {
// Fabric Loader is used at compile time only for the @Environment annotations,
// which Architectury remaps to the right annotation on each platform.
modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
modApi "dev.architectury:architectury:${rootProject.architectury_api_version}"
}
@@ -0,0 +1,35 @@
package xyz.nightsync.skinsg;
import net.minecraft.client.Minecraft;
import xyz.nightsync.skinsg.client.CapeManager;
import java.nio.file.Path;
/**
* Loader-agnostic client bootstrap. Each loader entrypoint calls {@link #init(Path)}
* with its own config directory, then wires the client command + client tick using
* {@link xyz.nightsync.skinsg.commands.SkinsgCommands} and {@link CapeManager}.
*/
public final class SkinsgClient {
private static SkinsgConfig config;
private SkinsgClient() {}
public static void init(Path configDir) {
config = SkinsgConfig.load(configDir);
CapeManager.init(config, () -> {
var p = Minecraft.getInstance().player;
return p != null ? p.getUUID() : null;
});
}
public static SkinsgConfig config() {
return config;
}
/** Call every client tick from the loader event. */
public static void tick() {
CapeManager.clientTick();
}
}
@@ -0,0 +1,115 @@
package xyz.nightsync.skinsg.commands;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.PlayerInfo;
import xyz.nightsync.skinsg.SkinsgConfig;
import xyz.nightsync.skinsg.client.CapeManager;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal;
import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument;
/**
* Loader-agnostic client command tree, built with RAW Brigadier builders so it is
* generic over the command source type {@code S}. Fabric client commands use
* {@code FabricClientCommandSource}; Forge/NeoForge use {@code CommandSourceStack}.
* Each loader supplies a {@link Feedback} that speaks to its own source, then
* registers {@code build(cfg, feedback)} into its client dispatcher.
*/
public final class SkinsgCommands {
/** Per-loader chat feedback so common code doesn't touch a loader source type. */
public interface Feedback<S> {
void success(S source, String message);
void failure(S source, String message);
}
public static final String ALL = "@a";
private SkinsgCommands() {}
public static <S> LiteralArgumentBuilder<S> build(SkinsgConfig cfg, Feedback<S> fb) {
SuggestionProvider<S> targets = (ctx, b) -> {
b.suggest(ALL);
var conn = Minecraft.getInstance().getConnection();
if (conn != null) {
for (PlayerInfo pi : conn.getOnlinePlayers()) b.suggest(pi.getProfile().getName());
}
return b.buildFuture();
};
return literal("skinsg")
.then(literal("playgif")
.executes(c -> { cfg.setNone(); CapeManager.releaseAll(); return ok(fb, c, "GIF capes: off"); })
.then(argument("target", StringArgumentType.word()).suggests(targets)
.executes(c -> playgif(fb, cfg, c))))
.then(literal("stopgif")
.executes(c -> { cfg.setNone(); CapeManager.releaseAll(); return ok(fb, c, "GIF capes: off"); })
.then(argument("target", StringArgumentType.word()).suggests(targets)
.executes(c -> stopgif(fb, cfg, c))))
.then(literal("self")
.executes(c -> { cfg.setSelf(); CapeManager.releaseAll(); return ok(fb, c, "GIF capes: self only"); }))
.then(literal("speed")
.then(argument("target", StringArgumentType.word()).suggests(targets)
.then(argument("percent", IntegerArgumentType.integer(1, 2000))
.executes(c -> speed(fb, cfg, c)))));
}
private static <S> int playgif(Feedback<S> fb, SkinsgConfig cfg, CommandContext<S> c) {
String target = StringArgumentType.getString(c, "target");
if (ALL.equals(target)) { cfg.setAll(); return ok(fb, c, "GIF capes: everyone"); }
Set<UUID> ids = resolve(target);
if (ids.isEmpty()) return err(fb, c, "player not found: " + target);
cfg.enableFor(ids);
return ok(fb, c, "GIF capes: on for " + target);
}
private static <S> int stopgif(Feedback<S> fb, SkinsgConfig cfg, CommandContext<S> c) {
String target = StringArgumentType.getString(c, "target");
if (ALL.equals(target)) { cfg.setNone(); CapeManager.releaseAll(); return ok(fb, c, "GIF capes: off"); }
Set<UUID> ids = resolve(target);
if (ids.isEmpty()) return err(fb, c, "player not found: " + target);
cfg.disableFor(ids);
for (UUID id : ids) CapeManager.releaseIfPresent(id);
return ok(fb, c, "GIF capes: stopped for " + target);
}
private static <S> int speed(Feedback<S> fb, SkinsgConfig cfg, CommandContext<S> c) {
String target = StringArgumentType.getString(c, "target");
int percent = IntegerArgumentType.getInteger(c, "percent");
if (ALL.equals(target)) { cfg.setGlobalSpeed(percent); return ok(fb, c, "GIF speed: " + percent + "% (all)"); }
Set<UUID> ids = resolve(target);
if (ids.isEmpty()) return err(fb, c, "player not found: " + target);
for (UUID id : ids) cfg.setSpeed(id, percent);
return ok(fb, c, "GIF speed: " + percent + "% for " + target);
}
private static Set<UUID> resolve(String name) {
Set<UUID> out = new HashSet<>();
var conn = Minecraft.getInstance().getConnection();
if (conn != null) {
for (PlayerInfo pi : conn.getOnlinePlayers()) {
if (pi.getProfile().getName().equalsIgnoreCase(name)) out.add(pi.getProfile().getId());
}
}
return out;
}
private static <S> int ok(Feedback<S> fb, CommandContext<S> c, String text) {
fb.success(c.getSource(), "[skinsg] " + text);
return 1;
}
private static <S> int err(Feedback<S> fb, CommandContext<S> c, String text) {
fb.failure(c.getSource(), "[skinsg] " + text);
return 0;
}
}