Skip to content
This repository was archived by the owner on Sep 21, 2026. It is now read-only.
Closed
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
64 changes: 64 additions & 0 deletions mods/guikit-fabric/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
plugins {
id 'net.fabricmc.fabric-loom' version '1.18.2'
id 'maven-publish'
}

version = project.mod_version
group = project.maven_group

base {
archivesName = project.archives_base_name
}

repositories {
// Loom automatically adds the essential Fabric + Minecraft repositories.
// Only add extra repositories here if you depend on other mods.
}

dependencies {
// Minecraft (26.3 is unobfuscated — do NOT declare mappings)
minecraft "com.mojang:minecraft:${project.minecraft_version}"

// Fabric Loader
implementation "net.fabricmc:fabric-loader:${project.loader_version}"

// Fabric API
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
}

processResources {
def version = project.version
inputs.property "version", version

filesMatching("fabric.mod.json") {
expand "version": version
}
}

tasks.withType(JavaCompile).configureEach {
it.options.release = 25
}

java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}

jar {
from("LICENSE") {
rename { "${it}_${project.base.archivesName.get()}" }
}
}

// configure the maven publication
publishing {
publications {
create("mavenJava", MavenPublication) {
from components.java
}
}
repositories {
// Add repositories to publish to here.
}
}
13 changes: 13 additions & 0 deletions mods/guikit-fabric/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx2G
org.gradle.parallel=true

# Fabric Properties - check https://fabricmc.net/develop for latest
minecraft_version=26.3
loader_version=0.19.5
fabric_version=0.161.0+26.3

# Mod Properties
mod_version=0.1.0
maven_group=net.runtoolkit
archives_base_name=guikit
9 changes: 9 additions & 0 deletions mods/guikit-fabric/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pluginManagement {
repositories {
maven { url 'https://maven.fabricmc.net/' }
mavenCentral()
gradlePluginPortal()
}
}

rootProject.name = 'guikit'
17 changes: 17 additions & 0 deletions mods/guikit-fabric/src/main/java/net/runtoolkit/guikit/Guikit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.runtoolkit.guikit;

import net.fabricmc.api.ModInitializer;
import net.runtoolkit.guikit.menu.GuikitMenus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Guikit implements ModInitializer {
public static final String MOD_ID = "guikit";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);

@Override
public void onInitialize() {
GuikitMenus.register();
LOGGER.info("guikit loaded (Fabric port, server-authoritative menus)");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.runtoolkit.guikit.cond;

import net.minecraft.resources.ResourceLocation;

import java.util.List;

/**
* Java port of storage guikit:cond {type:"...", ..., not:1b}.
* Every variant carries its own `not`, same as the datapack. Unlike the datapack, `not` is
* applied uniformly here (including on all/any) -- the datapack's README documented an
* inconsistent quirk on leaf types with an unknown key ("known quirk"); that quirk is not
* reproduced here, since nothing depended on it deliberately.
*/
public sealed interface Condition {
boolean not();

record Score(String objective, Integer min, Integer max, boolean not) implements Condition {}
record ItemCount(ResourceLocation item, int min, boolean not) implements Condition {}
record Tag(String tag, boolean not) implements Condition {}
record Gamemode(String mode, boolean not) implements Condition {}
record Advancement(ResourceLocation advancement, boolean not) implements Condition {}
record PredicateCond(ResourceLocation predicate, boolean not) implements Condition {}
record Level(Integer min, Integer max, boolean not) implements Condition {}
record Dimension(ResourceLocation dimension, boolean not) implements Condition {}
record All(List<Condition> of, boolean not) implements Condition {}
record Any(List<Condition> of, boolean not) implements Condition {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package net.runtoolkit.guikit.cond;

import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameType;

/**
* Java port of guikit:cond/check and the cond/t_*.mcfunction chain.
*
* UNVERIFIED, higher risk than the rest of this scaffold:
* - Score: player.getServer().getScoreboard() access pattern (getOrCreatePlayerScore /
* ScoreHolder / ScoreAccess) has changed shape across recent versions. Check against
* your exact Mojang-mapped Scoreboard class before relying on this.
* - Advancement: server.getAdvancements().get(ResourceLocation) -> AdvancementHolder, then
* player.getAdvancements().getOrStartProgress(holder).isDone() -- verify the exact method
* names for 26.3, this area of the API moves often.
* Everything else here (tag, gamemode, level, dimension, item count) is on stable,
* long-unchanged API and is lower risk.
*/
public final class ConditionChecker {
private ConditionChecker() {}

public static boolean check(ServerPlayer player, Condition cond) {
boolean result = evaluate(player, cond);
return cond.not() != result; // XOR
}

private static boolean evaluate(ServerPlayer player, Condition cond) {
return switch (cond) {
case Condition.Score c -> checkScore(player, c);
case Condition.ItemCount c -> countItem(player, c.item()) >= c.min();
case Condition.Tag c -> player.getTags().contains(c.tag());
case Condition.Gamemode c -> {
GameType wanted = switch (c.mode()) {
case "survival" -> GameType.SURVIVAL;
case "creative" -> GameType.CREATIVE;
case "adventure" -> GameType.ADVENTURE;
case "spectator" -> GameType.SPECTATOR;
default -> null;
};
yield wanted != null && player.gameMode.getGameModeForPlayer() == wanted;
}
case Condition.Level c -> {
int level = player.experienceLevel;
boolean min = c.min() == null || level >= c.min();
boolean max = c.max() == null || level <= c.max();
yield min && max;
}
case Condition.Dimension c -> player.level().dimension().location().equals(c.dimension());
case Condition.Advancement c -> checkAdvancement(player, c);
case Condition.PredicateCond c -> checkPredicate(player, c);
case Condition.All c -> {
for (Condition child : c.of()) {
if (!check(player, child)) yield false;
}
yield true; // empty list passes, same as the datapack
}
case Condition.Any c -> {
for (Condition child : c.of()) {
if (check(player, child)) yield true;
}
yield false; // no child passed (including the empty-list case), same as the datapack
}
};
}

private static boolean checkScore(ServerPlayer player, Condition.Score c) {
MinecraftServer server = player.getServer();
if (server == null) return false;
// UNVERIFIED -- see class javadoc. Sketch only:
// var score = server.getScoreboard().getOrCreatePlayerScore(player, objective);
// int value = score.get();
throw new UnsupportedOperationException(
"guikit: wire this to your Scoreboard API shape before use -- see ConditionChecker javadoc");
}

private static int countItem(ServerPlayer player, net.minecraft.resources.ResourceLocation item) {
int total = 0;
for (ItemStack stack : player.getInventory().items) {
if (!stack.isEmpty() && stack.getItem().builtInRegistryHolder().key().location().equals(item)) {
total += stack.getCount();
}
}
return total;
}

private static boolean checkAdvancement(ServerPlayer player, Condition.Advancement c) {
// UNVERIFIED -- see class javadoc.
throw new UnsupportedOperationException(
"guikit: wire this to your Advancements API shape before use -- see ConditionChecker javadoc");
}

private static boolean checkPredicate(ServerPlayer player, Condition.PredicateCond c) {
// UNVERIFIED -- LootContext-based predicate evaluation against player.
throw new UnsupportedOperationException(
"guikit: wire this to your predicate-evaluation API shape before use -- see ConditionChecker javadoc");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package net.runtoolkit.guikit.menu;

import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;

/**
* Fabric port of the whole chest_minecart mechanic.
*
* datapack: widget items are stamped onto a chest_minecart. Left-click moves the item OUT
* of the cart into the player's inventory; #guikit:fill re-runs to refill the empty slot;
* clear detects the item that left, the handler runs, item removed, redraw if dirty.
* A second function (internal/follow) keeps the cart teleporting onto the owning player
* every tick so it can't be walked away from or opened by someone else.
*
* Fabric: none of that exists. AbstractContainerMenu#clicked is the actual click trigger.
* We intercept it for the container's own slots (0..slots()-1), run the definition's
* onClick directly, and never call the vanilla pickup/move logic -- the item never leaves
* the slot, so there is nothing to "clear" or refill, and nothing to keep server-side state
* in sync with (no follow-teleport, no redraw-every-tick). Player-inventory slots (the
* bottom 36) keep normal vanilla behaviour so players can still manage their own items
* while a menu is open, matching what the datapack did by never touching the player's real
* inventory in the first place.
*/
public class GuikitMenu extends AbstractContainerMenu {
private static final int PLAYER_INV_SIZE = 36;

private final SimpleContainer container;
private final GuikitMenuDefinition definition;
private final ServerPlayer serverPlayer; // null on the client
private int page = 0;

public GuikitMenu(int syncId, Inventory playerInventory, GuikitMenuOpening opening) {
super(GuikitMenus.GUIKIT_MENU, syncId);
this.definition = GuikitMenuDefinitions.get(opening.menuId());
if (this.definition == null) {
throw new IllegalStateException("guikit: no menu registered for " + opening.menuId()
+ " (was #guikit:register equivalent -- call GuikitMenuDefinitions.register first)");
}
this.page = opening.page();
this.container = new SimpleContainer(definition.slots());
this.serverPlayer = playerInventory.player instanceof ServerPlayer sp ? sp : null;

int rows = definition.slots() / 9;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < 9; col++) {
int index = row * 9 + col;
addSlot(new Slot(container, index, 8 + col * 18, 18 + row * 18));
}
}
int invY = 18 + rows * 18 + 14;
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 9; col++) {
addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, invY + row * 18));
}
}
for (int col = 0; col < 9; col++) {
addSlot(new Slot(playerInventory, col, 8 + col * 18, invY + 58));
}

if (serverPlayer != null) {
redraw();
}
}

/** Was #guikit:fill: re-run the definition's draw for the current page. */
public void redraw() {
if (serverPlayer == null) return;
definition.draw(serverPlayer, container, page);
broadcastChanges();
}

/** Was function guikit:widget/goto_page: set the page and redraw, nothing else touches state. */
public void gotoPage(int newPage) {
this.page = newPage;
redraw();
}

@Override
public void clicked(int slotId, int button, ClickType clickType, Player player) {
boolean isContainerSlot = slotId >= 0 && slotId < definition.slots();
if (isContainerSlot && serverPlayer != null) {
definition.onClick(serverPlayer, this, slotId, page);
return; // never call super -- the widget item never moves or leaves the slot
}
super.clicked(slotId, button, clickType, player);
}

@Override
public ItemStack quickMoveStack(Player player, int index) {
// Shift-click on a widget slot must not extract it either.
return ItemStack.EMPTY;
}

@Override
public boolean stillValid(Player player) {
return true; // no chest_minecart to lose line-of-sight to; menu stays open until closed
}

public GuikitMenuDefinition definition() {
return definition;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.runtoolkit.guikit.menu;

import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;

/**
* One "menu" as the datapack defined it: a #guikit:register entry (title/size) plus the
* #guikit:fill listener (draw) plus the #guikit:probe listener (click dispatch). All three
* live together here instead of three separate data-driven tag callbacks.
*/
public interface GuikitMenuDefinition {
Component title();

/** Container size, e.g. 27 for a chest-sized menu (was the "container" registry's `slots`). */
int slots();

/** Fill the container for the given page. Was #guikit:fill + widget/pad + widget/draw. */
void draw(ServerPlayer player, Container container, int page);

/**
* A container slot was left-clicked. Was #guikit:probe: one {id, fn} line per clickable
* widget, matched against the slot the click came from. The click never moves or removes
* the item -- see GuikitMenu#clicked -- so there is no re-draw-after-every-click step
* unless this handler itself calls container.setChanges() / requests one.
*/
void onClick(ServerPlayer player, GuikitMenu menu, int slotIndex, int page);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.runtoolkit.guikit.menu;

import net.minecraft.resources.ResourceLocation;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* Replaces storage guikit:reg menus."ns:id" = {...}. A menu-providing mod calls
* GuikitMenuDefinitions.register(id, definition) from its own onInitialize -- the direct
* equivalent of a menu datapack adding itself to #guikit:register. No load-order tag needed:
* Fabric's onInitialize ordering + this being a plain map handles it.
*/
public final class GuikitMenuDefinitions {
private static final Map<ResourceLocation, GuikitMenuDefinition> REGISTRY = new LinkedHashMap<>();

private GuikitMenuDefinitions() {}

public static void register(ResourceLocation id, GuikitMenuDefinition definition) {
REGISTRY.put(id, definition);
}

public static GuikitMenuDefinition get(ResourceLocation id) {
return REGISTRY.get(id);
}
}
Loading
Loading