Skip to content
Draft
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
48 changes: 48 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ServerUtilities Architecture

ServerUtilities is a Minecraft Forge 1.7.10 mod and a compatibility-oriented backport of FTB Utilities, FTB
Library, and Aurora. Changes must preserve existing worlds, NBT data, network packet layouts, configuration keys,
and integration entry points unless a migration is explicitly designed.

## Lifecycle and state ownership

`Universe` is the server-lifecycle facade. Forge creates it before the server starts, loads persistent state after
the primary world is available, and closes it during shutdown. The object is registered on the Forge event buses,
so lifecycle and annotated event methods remain on the facade even when implementation details are delegated to
package-private components.

Players and teams belong to one `Universe`. Mutations must use their domain methods so cache invalidation, events,
and dirty-state propagation happen together. Read-only views are provided for inspection. Deprecated public
collections remain only as compatibility bridges and must not be used by project code.

## Persistence compatibility

Universe, player, and team data is stored as compressed NBT. Existing key names, event ordering, team UIDs, and
save order are compatibility contracts. Loading/import code may hydrate state without producing normal gameplay
events; ordinary mutations must use the public domain operations and mark affected objects dirty. NBT writes use a
sibling temporary file and atomic replacement where the platform supports it; dirty state is cleared only after a
successful replacement.

Backup restoration validates every archive entry against the chosen restore root. Both ZIP implementations share
the same path and filtering policy, while backend-specific code only adapts archive entry access. Restores extract
into an isolated staging directory before live files move, and a rollback journal restores every moved path if the
install does not complete. The journal is forced to disk before live mutations and recovered on the next restore if
the process previously stopped mid-transaction. Entry-count, extracted-size, free-space, link, and selected-world
boundaries are enforced by the shared archive layer.

## Threads and networking

Minecraft and Forge state is owned by the server thread unless a class explicitly documents otherwise. Background
backup, image, web, and file-I/O tasks must not mutate game state directly. Interrupted operations restore the
thread interrupt flag and stop or roll back their work. Backup runs share a single lifecycle token across threaded
and synchronous modes, and each run owns its original world save-state snapshot. Packet field order and NBT field
names are wire contracts.

## Supported integrations

New integrations should use `serverutils.api.ServerUtilitiesRegistry`. Its checked registration, duplicate-ID,
lookup, and read-only-view behavior are supported facade contracts. Extension value types retain their existing
compatibility guarantees; merely appearing in a facade signature does not make all of their implementation details a
stability promise. The facade is distributed in the main mod artifact, not as a standalone API-only artifact.
Supported extension points include reload handlers, team and admin actions, configuration value providers,
synchronized data, invsee inventories, and posted player/team/universe events.
33 changes: 33 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Contributing

## Development requirements

- Preserve Java 8 bytecode compatibility. Modern source syntax is provided by Jabel and does not raise the runtime
requirement.
- Build against the repository's Gradle wrapper and pinned GTNH dependencies.
- Keep changes focused and preserve existing NBT keys, configuration names, packet layouts, and public descriptors.

## Verification

Run these commands before submitting a change:

```powershell
.\gradlew.bat test
.\gradlew.bat spotlessCheck
.\gradlew.bat build
```

Add focused unit tests for pure logic and regression tests for corrected behavior. Forge lifecycle behavior that
cannot run in a plain unit test should be isolated behind a small package-private seam and covered with a server
smoke test.

## Code conventions

- Use domain mutation methods instead of writing public compatibility fields or collections.
- Use `Locale.ROOT` for identifiers, persisted names, permission nodes, and deterministic formatting.
- Missing optional data may fall back silently; malformed persisted/configuration data should log a warning;
unexpected failures should log an error with an ID or path. Do not use `printStackTrace` or empty catch blocks.
- Restore the interrupt flag when catching `InterruptedException`, and stop or roll back the interrupted operation.
- Use try-with-resources for streams, readers, archive handles, and other closeable objects.
- Add stable integration entry points under `serverutils.api`; retain deprecated bridges when compatibility requires
them.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Utilizes a permission system to handle whether a player can use certain commands
* [GTNHLib](https://www.curseforge.com/minecraft/mc-mods/gtnhlib)
* Optionally [Navigator](https://github.com/GTNewHorizons/Navigator) which enables claims and chunkload integration for JourneyMap or Xaeros World & Minimap

## Developer documentation

See [ARCHITECTURE.md](ARCHITECTURE.md) for lifecycle, persistence, threading, and API boundaries, and
[CONTRIBUTING.md](CONTRIBUTING.md) for setup, compatibility rules, and verification commands.

## Quick Install

Download the latest JAR from [releases](https://github.com/GTNewHorizons/ServerUtilities/releases) - you want the one named ServerUtilities-number.jar, not the `dev` or `sources`. Place it in the `/mods` folder on your server AND client (if single player, on client only).
Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
plugins {
id 'com.gtnewhorizons.gtnhconvention'
}

tasks.named('test') {
useJUnitPlatform()
}
4 changes: 4 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ dependencies {
compileOnly(rfg.deobf("curse.maven:witchery-69673:2234410")) {transitive = false}

runtimeOnlyNonPublishable("com.github.GTNewHorizons:waila:1.19.30:dev")

testImplementation('org.junit.jupiter:junit-jupiter:5.10.2')
testRuntimeOnly('it.unimi.dsi:fastutil:8.5.18')
testRuntimeOnly('org.junit.platform:junit-platform-launcher:1.10.2')
}
2 changes: 0 additions & 2 deletions src/main/java/serverutils/ServerUtilities.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package serverutils;

import java.util.Locale;
import java.util.Map;

import javax.annotation.Nullable;
Expand Down Expand Up @@ -65,7 +64,6 @@ public static CommandException errorFeatureDisabledServer(@Nullable ICommandSend

@Mod.EventHandler
public void onPreInit(FMLPreInitializationEvent event) {
Locale.setDefault(Locale.US);
PROXY.preInit(event);
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/serverutils/ServerUtilitiesCommon.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public void init(FMLInitializationEvent event) {
ServerUtilitiesRegistry.registerDefaults();
ServerUtilitiesPermissions.init();
CHAT_FORMATTING_SUBSTITUTES.put("name", ForgePlayer::getDisplayName);
CHAT_FORMATTING_SUBSTITUTES.put("team", player -> player.team.getTitle());
CHAT_FORMATTING_SUBSTITUTES.put("team", player -> player.getTeam().getTitle());
}

public void postInit(FMLPostInitializationEvent event) {
Expand Down Expand Up @@ -146,7 +146,7 @@ private static void registerBrigadierCommands(LiteralCommandNode<?> literalNode,
? Rank.NODE_COMMAND + '.' + cmdPerm.serverutilities$getModId() + "." + literalNode.getLiteral()
: parentNode + "." + literalNode.getLiteral();

cmdPerm.serverutilities$setPermissionNode(node.toLowerCase());
cmdPerm.serverutilities$setPermissionNode(node.toLowerCase(java.util.Locale.ROOT));
cmdPerm.serverUtilities$registerPermissions();

for (CommandNode<?> child : literalNode.getChildren()) {
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/serverutils/ServerUtilitiesLeaderboards.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ static void loadLeaderboards() {
new ChatComponentTranslation("serverutilities.stat.dph"),
player -> {
double d = getDPH(player);
return new ChatComponentText(d < 0D ? "-" : String.format("%.2f", d));
return new ChatComponentText(
d < 0D ? "-" : String.format(java.util.Locale.ROOT, "%.2f", d));
},
Comparator.comparingDouble(ServerUtilitiesLeaderboards::getDPH).reversed(),
player -> getDPH(player) >= 0D));
Expand Down Expand Up @@ -71,7 +72,7 @@ static void loadLeaderboards() {
component.getChatStyle().setColor(EnumChatFormatting.GREEN);
return component;
} else {
long worldTime = player.team.universe.world.getTotalWorldTime();
long worldTime = player.getUniverse().world.getTotalWorldTime();
long time = worldTime - player.getLastTimeSeen();
return Leaderboard.FromStat.LONG_TIME.apply(time);
}
Expand Down Expand Up @@ -99,7 +100,7 @@ private static long getRelativeLastSeen(ForgePlayer player) {
return 0;
}

return player.team.universe.ticks.ticks() - player.getLastTimeSeen();
return player.getUniverse().ticks.ticks() - player.getLastTimeSeen();
}

private static double getDPH(ForgePlayer player) {
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/serverutils/ServerUtilitiesPermissions.java
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,14 @@ public static void registerPrefix(String node, DefaultPermissionLevel level, Str

public static String formatId(@Nullable Block item) {
return (item == null || GameData.getBlockRegistry().getNameForObject(item) == null) ? "minecraft.air"
: GameData.getBlockRegistry().getNameForObject(item).toLowerCase().replace(':', '.');
: GameData.getBlockRegistry().getNameForObject(item).toLowerCase(java.util.Locale.ROOT)
.replace(':', '.');
}

public static String formatId(@Nullable Item item) {
return (item == null || GameData.getItemRegistry().getNameForObject(item) == null) ? "minecraft.air"
: GameData.getItemRegistry().getNameForObject(item).toLowerCase().replace(':', '.');
: GameData.getItemRegistry().getNameForObject(item).toLowerCase(java.util.Locale.ROOT)
.replace(':', '.');
}

public static boolean hasBlockEditingPermission(EntityPlayer player, Block block) {
Expand Down
37 changes: 34 additions & 3 deletions src/main/java/serverutils/ServerUtilitiesRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,32 +61,63 @@
@SuppressWarnings("unused")
public class ServerUtilitiesRegistry {

@Deprecated
public static final Map<ResourceLocation, IReloadHandler> RELOAD_IDS = new HashMap<>();
@Deprecated
public static final Map<ResourceLocation, TeamAction> TEAM_GUI_ACTIONS = new HashMap<>();
@Deprecated
public static final Map<ResourceLocation, AdminPanelAction> ADMIN_PANEL_ACTIONS = new HashMap<>();
@Deprecated
public static final Map<String, ConfigValueProvider> CONFIG_VALUE_PROVIDERS = new HashMap<>();
@Deprecated
public static final Map<String, ISyncData> SYNCED_DATA = new HashMap<>();

/**
* @deprecated Use
* {@link serverutils.api.ServerUtilitiesRegistry#registerConfigValueProvider(String, ConfigValueProvider)}.
*/
@Deprecated
public static void registerConfigValueProvider(String id, ConfigValueProvider provider) {
CONFIG_VALUE_PROVIDERS.put(id, provider);
}

/**
* @deprecated Use {@link serverutils.api.ServerUtilitiesRegistry#registerSyncData(String, ISyncData)}.
*/
@Deprecated
public static void registerSyncData(String mod, ISyncData data) {
SYNCED_DATA.put(mod, data);
}

/**
* @deprecated Use
* {@link serverutils.api.ServerUtilitiesRegistry#registerServerReloadHandler(ResourceLocation, IReloadHandler)}.
*/
@Deprecated
public static void registerServerReloadHandler(ResourceLocation id, IReloadHandler handler) {
RELOAD_IDS.put(id, handler);
}

/**
* @deprecated Use {@link serverutils.api.ServerUtilitiesRegistry#registerAdminPanelAction(AdminPanelAction)}.
*/
@Deprecated
public static void registerAdminPanelAction(AdminPanelAction action) {
ADMIN_PANEL_ACTIONS.put(action.getId(), action);
}

/**
* @deprecated Use {@link serverutils.api.ServerUtilitiesRegistry#registerTeamAction(TeamAction)}.
*/
@Deprecated
public static void registerTeamAction(TeamAction action) {
TEAM_GUI_ACTIONS.put(action.getId(), action);
}

/**
* @deprecated Use {@link serverutils.api.ServerUtilitiesRegistry#registerInvseeInventory(IModdedInventory)}.
*/
@Deprecated
@ApiStatus.AvailableSince("2.4.0")
public static void registerInvseeInventory(IModdedInventory inventory) {
InvSeeRegistry.registerInventory(inventory);
Expand Down Expand Up @@ -153,7 +184,7 @@ public Type getType(ForgePlayer player, NBTTagCompound data) {
@Override
public void onAction(ForgePlayer player, NBTTagCompound data) {
ServerUtilitiesAPI.reloadServer(
player.team.universe,
player.getUniverse(),
player.getPlayer(),
EnumReloadType.RELOAD_COMMAND,
ServerReloadEvent.ALL);
Expand All @@ -169,7 +200,7 @@ public Type getType(ForgePlayer player, NBTTagCompound data) {

@Override
public void onAction(ForgePlayer player, NBTTagCompound data) {
new MessageViewCrashList(player.team.universe.server.getFile("crash-reports"))
new MessageViewCrashList(player.getUniverse().server.getFile("crash-reports"))
.sendTo(player.getPlayer());
}
});
Expand All @@ -190,7 +221,7 @@ public void onAction(ForgePlayer player, NBTTagCompound data) {
ConfigGroup gamerules = main.getGroup("gamerules");
gamerules.setDisplayName(new ChatComponentTranslation("gamerules"));

GameRules rules = player.team.universe.world.getGameRules();
GameRules rules = player.getUniverse().world.getGameRules();

for (String key : rules.getRules()) {
String value = rules.getGameRuleStringValue(key);
Expand Down
Loading