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
91 changes: 91 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Mellow is a Minecraft 1.8.9 Forge client mod for Hypixel BedWars & Duels stats checking. It's a fork of [Fontaine](https://github.com/xanning/Fontaine), using OneConfig for GUI (Right Shift to open). No API key required for core features.

## Build Commands

```bash
# Requires Java 21 locally (CI uses Java 17)
export JAVA_HOME=$(/usr/libexec/java_home -v 21)

# Build (also runs tests, copies jar to PrismLauncher mods dir automatically)
./gradlew build

# Tests only (JUnit 4)
./gradlew test
```

Output jar: `versions/1.8.9-forge/build/libs/Mellow-1.8.9-forge-{version}.jar`

## Build System

Polyfrost Gradle Toolkit with multi-version support (currently only 1.8.9-forge active). Uses ShadowJar to shade OkHttp3, Hypixel Mod API, and XZ. Blossom for token replacement. Mixin 0.7.11 for bytecode injection. Target compatibility is Java 8.

## Architecture

### Entry Point
`Mellow.java` — `@Mod` class. Initializes all services, registers 19 commands and 7 event routers on the Forge event bus. Static fields hold singleton services accessed throughout the codebase.

- `Mellow.isEnabled()` — central mod toggle check. **All features must gate on this** before doing work.
- `Mellow.config` — the `MellowOneConfig` instance (OneConfig-based, ~1600 lines).

### Event Flow
Forge events → **Event Routers** (`core/event/`) → **Feature Services**

Routers are thin dispatchers registered on `MinecraftForge.EVENT_BUS`:
- `ChatEventRouter` — chat messages → denicker, pregame stats, request popups, auto /who
- `ClientTickRouter` — per-tick → HypixelFeatures game state, replay manager
- `TabOverlayRouter` — tab key rendering → extended stats tab overlay
- `WorldLifecycleRouter` — world load → cache clearing
- `NametagColorRouter` — pre/post render → nametag color + client icon context
- `RequestPopupRouter` — overlay render + keybind input → popup accept/deny
- `ReplayHudRouter` / `ReplayInputRouter` — replay UI

### Game State
`HypixelFeatures` (singleton) tracks Hypixel server state via scoreboard/tab parsing. Produces `GameSnapshot` objects consumed by listeners via `addGameStateListener()`. This runs even when the mod is toggled off so state is available on re-enable.

### Stats Pipeline
1. **ProviderManager** selects active provider (HypixelPublicApi, NadeshikoApi, AbyssApi)
2. **PlayerCache** fetches/caches profiles with scoped stat requests (`StatScope`)
3. **StatsChecker** formats stats for display
4. **InGameTabStatsSyncService** pushes stats into `Mellow.tabStats` map (consumed by tab mixin)
5. **GuiPlayerTabOverlayMixin** reads `tabStats` to modify vanilla tab list names

### Async Execution
`AsyncExecutor` singleton manages 5 thread pools: `profileIo` (8), `chat` (4), `command` (4), `supplementalIo` (4), `replayIo` (1). `MainThreadDispatcher` posts runnables back to the client thread.

### Mixin Layer (`mixin/`)
- `GuiPlayerTabOverlayMixin` — injects stats into tab list player names
- `PingMixin` — overrides `NetworkPlayerInfo.getResponseTime` with external ping data
- `nametag/` — color backgrounds + client icon rendering on nametags
- `hitbox/` — team-colored hitboxes
- `replay/` — packet capture via `NetworkManager` injection
- `compat/` — compatibility with PolyNametag, PolyHitbox, VanillaHUD, Overflow Animations

### API Integrations (`api/`)
External services with their own caching: Aurora (denicking, pings, winstreaks), Seraph (client detection, pings, tags), Urchin (tags), Luna (pings), Mojang (UUID/profile), Hypixel Mod API (game state).

### Feature Modules (`feature/`)
- `stats/` — pregame stats, in-game tab sync, extended tab overlay with scrolling
- `replay/` — packet-level game recording/playback with seeking, browser GUI
- `nicks/` — skin denicking + number-pattern denicking
- `requestpopup/` — friend/party request accept/deny overlay
- `profileviewer/` — `/pv` GUI with parsed stats
- `party/` — blacklisted party member warnings
- `tags/` — Urchin/Seraph tag display

### Lists (`util/`)
Blacklist, annoylist, and tag-ignore — file-backed with import support. Stored in `.minecraft/config/mellow/`.

## Key Conventions

- Config lives in one large `MellowOneConfig` class with OneConfig annotations (`@Switch`, `@Dropdown`, `@Number`, etc.)
- Commands extend Forge's `ICommand`, registered in `Mellow.init()`
- Mixin config: `src/main/resources/mixins.mellow.json`
- Assets: `src/main/resources/assets/mellow/` (textures for client icons, GUI, socials)
- All API calls should go through the async executor, never on the main thread
- Feature gating pattern: check `Mellow.isEnabled()` at event handler / mixin entry points
16 changes: 5 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -222,23 +222,17 @@ tasks.named("build") {

// Ensure the built JAR file exists before proceeding
if (finalJar.exists()) {
// Additional destination directory
val home = System.getProperty("user.home")
val additionalDestDir =
val modrinthDestDir =
file(
"$home/Library/Application Support/PrismLauncher/instances/1.8.9/.minecraft/mods"
"$home/Library/Application Support/ModrinthApp/profiles/1.8.9/mods"
)

// Ensure the destination directory exists
additionalDestDir.mkdirs()

// Copy the final JAR to the additional directory
modrinthDestDir.mkdirs()
copy {
from(finalJar)
into(additionalDestDir)
into(modrinthDestDir)
}

println("JAR file copied to: ${additionalDestDir.absolutePath}")
println("JAR file copied to: ${modrinthDestDir.absolutePath}")
} else {
println("Built JAR file does not exist: ${finalJar.absolutePath}")
}
Expand Down
85 changes: 34 additions & 51 deletions src/main/java/com/roxiun/mellow/Mellow.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.command.ICommand;
import net.minecraftforge.client.ClientCommandHandler;
import net.minecraftforge.fml.client.registry.ClientRegistry;
import net.minecraftforge.common.MinecraftForge;
Expand Down Expand Up @@ -224,57 +225,25 @@ public void run() {
new TabOverlayInputRouter(tabOverlayRouter)
);

ClientCommandHandler.instance.registerCommand(
new BedwarsCommand(playerCache, config)
);
ClientCommandHandler.instance.registerCommand(
new SkywarsCommand(playerCache, config)
);
ClientCommandHandler.instance.registerCommand(
new PVCommand(playerCache, config)
);
ClientCommandHandler.instance.registerCommand(new MellowCommand());
ClientCommandHandler.instance.registerCommand(new DebugStateCommand());
ClientCommandHandler.instance.registerCommand(
new ClearCacheCommand(playerCache, tabStats)
);
ClientCommandHandler.instance.registerCommand(
new RefreshCommand(inGameTabStatsSyncService)
);
ClientCommandHandler.instance.registerCommand(
new DenickCommand(config, auroraApi)
);
ClientCommandHandler.instance.registerCommand(
new SkinDenickCommand(playerCache)
);
ClientCommandHandler.instance.registerCommand(
new BlacklistCommand(blacklistManager, mojangApi)
);
ClientCommandHandler.instance.registerCommand(
new AnnoylistCommand(annoylistManager, mojangApi)
);
ClientCommandHandler.instance.registerCommand(
new TagIgnoreCommand(tagIgnoreManager, mojangApi)
);
ClientCommandHandler.instance.registerCommand(
new UrchinCommand(urchinApi, mojangApi, config)
);
ClientCommandHandler.instance.registerCommand(
new SeraphCommand(seraphApi, mojangApi, config)
);
ClientCommandHandler.instance.registerCommand(
new StatusCommand(mojangApi, config)
);
ClientCommandHandler.instance.registerCommand(
new NameHistoryCommand(mojangApi)
);
ClientCommandHandler.instance.registerCommand(
new ClientCommand(seraphApi, mojangApi, config)
);
ClientCommandHandler.instance.registerCommand(
new WinstreakCommand(playerCache, config)
);
ClientCommandHandler.instance.registerCommand(new ReplayCommand(replayManager));
registerCommand(new BedwarsCommand(playerCache, config));
registerCommand(new SkywarsCommand(playerCache, config));
registerCommand(new PVCommand(playerCache, config));
registerCommand(new MellowCommand());
registerCommand(new DebugStateCommand());
registerCommand(new ClearCacheCommand(playerCache, tabStats));
registerCommand(new RefreshCommand(inGameTabStatsSyncService));
registerCommand(new DenickCommand(config, auroraApi));
registerCommand(new SkinDenickCommand(playerCache));
registerCommand(new BlacklistCommand(blacklistManager, mojangApi));
registerCommand(new AnnoylistCommand(annoylistManager, mojangApi));
registerCommand(new TagIgnoreCommand(tagIgnoreManager, mojangApi));
registerCommand(new UrchinCommand(urchinApi, mojangApi, config));
registerCommand(new SeraphCommand(seraphApi, mojangApi, config));
registerCommand(new StatusCommand(mojangApi, config));
registerCommand(new NameHistoryCommand(mojangApi));
registerCommand(new ClientCommand(seraphApi, mojangApi, config));
registerCommand(new WinstreakCommand(playerCache, config));
registerCommand(new ReplayCommand(replayManager));
}

public StatsProvider getStatsProvider() {
Expand All @@ -284,7 +253,21 @@ public StatsProvider getStatsProvider() {
return providerManager.getSelectedProvider(config);
}

/**
* Central mod-enabled check. All features, event handlers, mixins,
* and HUD elements should gate on this before doing any work.
*/
public static boolean isEnabled() {
return config != null && config.modEnabled;
}

public static AnticheatManager getAnticheatManager() {
return anticheatManager;
}

private void registerCommand(ICommand command) {
ClientCommandHandler
.instance
.registerCommand(CommandToggleWrapper.wrap(command));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void onClientTick(TickEvent.ClientTickEvent event) {

@SubscribeEvent
public void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (!Mellow.config.anticheatEnabled) return;
if (!Mellow.isEnabled() || !Mellow.config.anticheatEnabled) return;

if (event.phase == TickEvent.Phase.START) {
ACPlayerData data = manager.getPlayerData(event.player);
Expand Down Expand Up @@ -114,7 +114,7 @@ public void onPlayerTick(TickEvent.PlayerTickEvent event) {

@SubscribeEvent
public void onEntityJoinWorld(EntityJoinWorldEvent event) {
if (!Mellow.config.anticheatEnabled) return;
if (!Mellow.isEnabled() || !Mellow.config.anticheatEnabled) return;
if (event.entity instanceof EntityPlayer) {
manager.registerPlayer((EntityPlayer) event.entity);
}
Expand Down
81 changes: 81 additions & 0 deletions src/main/java/com/roxiun/mellow/commands/CommandToggleWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.roxiun.mellow.commands;

import com.roxiun.mellow.Mellow;
import com.roxiun.mellow.util.ChatUtils;
import java.util.Collections;
import java.util.List;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommand;
import net.minecraft.command.ICommandSender;
import net.minecraft.util.BlockPos;

public class CommandToggleWrapper implements ICommand {

private final ICommand delegate;

private CommandToggleWrapper(ICommand delegate) {
this.delegate = delegate;
}

public static ICommand wrap(ICommand command) {
if (command instanceof CommandToggleWrapper) {
return command;
}
return new CommandToggleWrapper(command);
}

@Override
public String getCommandName() {
return delegate.getCommandName();
}

@Override
public String getCommandUsage(ICommandSender sender) {
return delegate.getCommandUsage(sender);
}

@Override
public List<String> getCommandAliases() {
return delegate.getCommandAliases();
}

@Override
public void processCommand(ICommandSender sender, String[] args)
throws CommandException {
if (!Mellow.isEnabled()) {
ChatUtils.sendCommandMessage(
sender,
"§cMellow is disabled. Enable it in the config to use commands."
);
return;
}
delegate.processCommand(sender, args);
}

@Override
public boolean canCommandSenderUseCommand(ICommandSender sender) {
return Mellow.isEnabled() && delegate.canCommandSenderUseCommand(sender);
}

@Override
public List<String> addTabCompletionOptions(
ICommandSender sender,
String[] args,
BlockPos pos
) {
if (!Mellow.isEnabled()) {
return Collections.emptyList();
}
return delegate.addTabCompletionOptions(sender, args, pos);
}

@Override
public boolean isUsernameIndex(String[] args, int index) {
return delegate.isUsernameIndex(args, index);
}

@Override
public int compareTo(ICommand other) {
return delegate.compareTo(other);
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/roxiun/mellow/config/MellowOneConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@

public class MellowOneConfig extends Config {

@Switch(name = "Mod Enabled", subcategory = "General", description = "Master toggle to enable or disable the entire mod.")
public boolean modEnabled = true;

@Switch(name = "Auto /who", subcategory = "General")
public boolean autoWho = false;

@Switch(name = "Hide Auto /who Response", subcategory = "General", description = "Hides the ONLINE: player list response when auto /who is triggered.")
public boolean hideAutoWhoResponse = true;

@Switch(name = "Show Tab Stats", subcategory = "General")
public boolean tabStats = true;

Expand Down
Loading