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
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
strategy:
matrix:
game_version: [ # Update this when adding new game versions!
"1.21.11",
"1.21.6",
"1.21.5",
"1.21.4",
Expand Down
6 changes: 4 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import xyz.srnyx.gradlegalaxy.utility.setupJava

plugins {
java
id("fabric-loom") version "1.11-SNAPSHOT"
id("fabric-loom") version "1.14-SNAPSHOT"
id("xyz.srnyx.gradle-galaxy") version "2.1.0"
}

Expand Down Expand Up @@ -37,10 +37,12 @@ dependencies {
if (hasProperty("deps.placeholder_api")) dependencies.modCompileOnly("eu.pb4", "placeholder-api", property("deps.placeholder_api").toString())

// Replacements for fabric.mod.json and config.json
val mixinConfig = if (stonecutter.current.version == "1.21.11") "eventutils-1.21.11.mixin.json" else "eventutils.mixin.json"
addReplacementsTask(setOf("fabric.mod.json"), getDefaultReplacements() + mapOf(
"mod_name" to property("mod.name").toString(),
"mod_version" to property("mod.version").toString(),
"deps_minecraft" to property("deps.minecraft").toString()))
"deps_minecraft" to property("deps.minecraft").toString(),
"mixin_config" to mixinConfig))

base.archivesName = name

Expand Down
80 changes: 80 additions & 0 deletions crop_sheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Crop sheet.png (3 cols x 2 rows) into individual plus-tag icons with transparent background."""
from pathlib import Path

try:
from PIL import Image
except ImportError:
print("Install Pillow: pip install Pillow")
raise

# Order: row0 = bee, white, linked; row1 = booster, contrib, admin
NAMES = ["bee", "white", "linked", "booster", "contrib", "admin"]
ICON_SIZE = 64 # match PlusTagRenderer.TEX_SIZE for sharp in-game scaling
# Pixels within this distance of the sheet background color become transparent (0–255 per channel)
BG_TOLERANCE = 25

SCRIPT_DIR = Path(__file__).resolve().parent
OUT_DIR = SCRIPT_DIR / "src" / "main" / "resources" / "assets" / "eventutils" / "textures" / "gui"
# Prefer sheet in project root, then plus_sheet in gui folder
SHEET_CANDIDATES = [SCRIPT_DIR / "sheet.png", OUT_DIR / "plus_sheet.png"]


def make_bg_transparent(img: Image.Image, bg_rgba: tuple, tolerance: int) -> Image.Image:
"""Replace pixels matching the background color (within tolerance) with transparent."""
data = img.getdata()
r0, g0, b0, a0 = bg_rgba
out = []
for p in data:
if len(p) == 3:
r, g, b = p
if abs(r - r0) <= tolerance and abs(g - g0) <= tolerance and abs(b - b0) <= tolerance:
out.append((0, 0, 0, 0))
else:
out.append((r, g, b, 255))
else:
r, g, b, a = p
if abs(r - r0) <= tolerance and abs(g - g0) <= tolerance and abs(b - b0) <= tolerance:
out.append((0, 0, 0, 0))
else:
out.append((r, g, b, a))
img.putdata(out)
return img


def main():
SHEET = next((p for p in SHEET_CANDIDATES if p.exists()), None)
if SHEET is None:
print(f"Not found: tried {SHEET_CANDIDATES}")
return 1
print(f"Using sheet: {SHEET}")
img = Image.open(SHEET).convert("RGBA")
w, h = img.size
# Use top-left corner as background color
bg_rgba = img.getpixel((0, 0))
if len(bg_rgba) == 3:
bg_rgba = (bg_rgba[0], bg_rgba[1], bg_rgba[2], 255)
print(f"Background color (will be made transparent): {bg_rgba}")

col_w = w // 3
row_h = h // 2
OUT_DIR.mkdir(parents=True, exist_ok=True)
idx = 0
for row in range(2):
for col in range(3):
x = col * col_w
y = row * row_h
cw = (w - x) if col == 2 else col_w
ch = (h - y) if row == 1 else row_h
crop = img.crop((x, y, x + cw, y + ch))
crop = crop.resize((ICON_SIZE, ICON_SIZE), Image.Resampling.LANCZOS)
crop = make_bg_transparent(crop, bg_rgba, BG_TOLERANCE)
out_path = OUT_DIR / f"{NAMES[idx]}.png"
crop.save(out_path)
print(f"Saved {out_path.name} ({ICON_SIZE}x{ICON_SIZE}, transparent bg)")
idx += 1
print("Done.")
return 0

if __name__ == "__main__":
raise SystemExit(main())
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ stonecutter {
centralScript = "build.gradle.kts"
shared {
versions( // Make sure to update .github/workflows/publish.yml when changing versions!
"1.21.11",
"1.21.6",
"1.21.5",
"1.21.4",
Expand Down
Binary file added sheet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/main/java/cc/aabss/eventutils/EventInfoScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ public class EventInfoScreen extends Screen {
@NotNull private final JsonObject json;

public EventInfoScreen(@NotNull JsonObject json) {
//? if >=1.21.11 {
/*super(Text.translatable(EventUtils.MOD.keybindManager.eventInfoKey.getId()));
*///?} else {
super(Text.translatable(EventUtils.MOD.keybindManager.eventInfoKey.getTranslationKey()));
//?}
this.json = json;
}

Expand Down
17 changes: 10 additions & 7 deletions src/main/java/cc/aabss/eventutils/EventServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public void setServerList(@Nullable ServerList serverList) {
this.serverList = serverList;
}

public void addEventServer(@NotNull JsonObject eventJson, @NotNull String ip) {
public void addEventServer(@NotNull EventType eventType, @NotNull JsonObject eventJson, @NotNull String ip) {
if (!mod.config.eventServersEnabled) return;
if (!mod.config.eventServerTypes.contains(eventType)) return;
final MinecraftClient client = MinecraftClient.getInstance();
if (client == null) return;

Expand Down Expand Up @@ -94,9 +96,10 @@ public void addEventServer(@NotNull JsonObject eventJson, @NotNull String ip) {
final EventServerInfo eventServerInfo = new EventServerInfo(finalEventId, serverInfo, finalEventTime);
activeEventServers.put(finalEventId, eventServerInfo);

// Schedule removal 5 minutes after event starts
// Schedule removal after configurable grace period (default 5 minutes)
final long currentTime = System.currentTimeMillis();
final long graceMs = TimeUnit.MINUTES.toMillis(5);
final int displayMinutes = mod.config.getEventServerDisplayMinutes();
final long graceMs = TimeUnit.MINUTES.toMillis(displayMinutes);
final long timeUntilRemoval = (finalEventTime + graceMs) - currentTime;

if (timeUntilRemoval > 0) {
Expand All @@ -105,21 +108,21 @@ public void addEventServer(@NotNull JsonObject eventJson, @NotNull String ip) {
timeUntilRemoval,
TimeUnit.MILLISECONDS);
removalTasks.put(finalEventId, removalTask);
EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms (5m after start)", finalTitle, timeUntilRemoval);
EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms ({}m after start)", finalTitle, timeUntilRemoval, displayMinutes);
} else {
// If within 5-minute grace after event start, keep it briefly; else do not add
// If within grace period after event start, keep it briefly; else do not add
if (currentTime - finalEventTime <= graceMs) {
final long remaining = graceMs - (currentTime - finalEventTime);
final ScheduledFuture<?> removalTask = mod.scheduler.schedule(
() -> removeEventServer(finalEventId),
remaining,
TimeUnit.MILLISECONDS);
removalTasks.put(finalEventId, removalTask);
EventUtils.LOGGER.info("Event '{}' already started; keeping for {} ms (grace)", finalTitle, remaining);
EventUtils.LOGGER.info("Event '{}' already started; keeping for {} ms ({}m grace)", finalTitle, remaining, displayMinutes);
} else {
serverList.remove(serverInfo);
activeEventServers.remove(finalEventId);
EventUtils.LOGGER.info("Event '{}' started more than 5 minutes ago; not adding", finalTitle);
EventUtils.LOGGER.info("Event '{}' started more than {} minutes ago; not adding", finalTitle, displayMinutes);
return;
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/cc/aabss/eventutils/EventType.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ public Option<NotificationSound> getSoundOption(@NotNull EventConfig config) {
.build();
}

@NotNull
public Option<Boolean> getServerListOption(@NotNull EventConfig config) {
return Option.<Boolean>createBuilder()
.name(displayName)
.description(OptionDescription.of(Text.of(EventUtils.translate("eventutils.config.server_list_event_description").replace("{event}", displayName.getString()))))
.binding(true, () -> config.eventServerTypes.contains(this), newValue -> {
if (newValue) {
if (!config.eventServerTypes.contains(this)) config.eventServerTypes.add(this);
} else {
config.eventServerTypes.remove(this);
}
config.setSave("event_server_types", config.eventServerTypes);
})
.controller(ConfigScreen::getBooleanBuilder)
.build();
}

public void sendToast(@NotNull EventUtils mod, @Nullable Integer prize, boolean hasIp) {
final MinecraftClient client = MinecraftClient.getInstance();
if (client == null) return;
Expand Down
80 changes: 78 additions & 2 deletions src/main/java/cc/aabss/eventutils/EventUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import cc.aabss.eventutils.websocket.SocketEndpoint;
import cc.aabss.eventutils.websocket.WebSocketClient;
import cc.aabss.eventutils.config.EventConfig;
import cc.aabss.eventutils.config.PlayerGroup;
import cc.aabss.eventutils.plustag.EventAlertsApi;

import com.google.gson.JsonObject;

Expand Down Expand Up @@ -59,7 +61,8 @@ public class EventUtils implements ClientModInitializer {
public KeybindManager keybindManager;
@NotNull public final EventServerManager eventServerManager = new EventServerManager(this);
@NotNull public final Map<EventType, String> lastIps = new EnumMap<>(EventType.class);
public boolean hidePlayers = false;
/** 0 = first group (or hide-all when no groups), 1 = second group, ... ; groups.size() = players revealed */
public int hidePlayersViewMode = 0;

public EventUtils() {
MOD = this;
Expand All @@ -85,6 +88,21 @@ public void onInitializeClient() {
// Update checker
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> updateChecker.checkUpdate());

// Fetch Event Alerts plus tags for local player
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
if (client.player != null) {
var uuid = client.player.getUuid();
LOGGER.info("[EventUtils] JOIN: scheduling Event Alerts fetch for local player uuid={}", uuid);
EventAlertsApi.scheduleFetchIfNeeded(uuid.toString());
} else {
LOGGER.info("[EventUtils] JOIN: client.player is null, skipping fetch (will retry when tab list is opened)");
}
});
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
LOGGER.info("[EventUtils] DISCONNECT: clearing Event Alerts cache");
EventAlertsApi.clearCache();
});

// Initialize keybind manager
keybindManager = new KeybindManager(this);

Expand Down Expand Up @@ -139,13 +157,71 @@ public String getIpAndConnect(@NotNull EventType eventType, @NotNull JsonObject
}

public static boolean isNPC(@NotNull String name, boolean bypass) {
return (!MOD.config.hideNPCs || bypass) && (name.contains("[") || name.contains("]") || name.contains(" ") || name.contains("-") || name.equals("§z"));
return (MOD.config.hideNPCs || bypass) && looksLikeNPC(name);
}

public static boolean isNPC(@NotNull String name) {
return isNPC(name, false);
}

public static boolean looksLikeNPC(@NotNull String name) {
return name.contains("[") || name.contains("]") || name.contains(" ") || name.contains("-") || name.equals("§z");
}

/** Whether the current view mode is "players revealed" (show everyone). */
public boolean isHidePlayersRevealed() {
final int n = config.groups.size();
if (n == 0) return hidePlayersViewMode == 1;
return hidePlayersViewMode >= n;
}

/** Whether we are in a "hide" mode (any group or hide-all). */
public boolean isInHidePlayersMode() {
final int n = config.groups.size();
if (n == 0) return hidePlayersViewMode == 0;
return hidePlayersViewMode < n;
}

/** Current group when in group view mode, or null if revealed or no groups. */
@Nullable
public PlayerGroup getCurrentViewGroup() {
final var groups = config.groups;
if (groups.isEmpty() || hidePlayersViewMode >= groups.size()) return null;
return groups.get(hidePlayersViewMode);
}

/**
* True if the player (by lowercased name) should be visible with current view mode.
* Caller must exclude main player.
*/
public boolean isPlayerVisible(@NotNull String nameLower) {
if (isHidePlayersRevealed()) return true;
if (config.whitelistedPlayers.contains(nameLower)) return true;
final PlayerGroup group = getCurrentViewGroup();
final boolean isNpc = looksLikeNPC(nameLower);

// NPC behavior: if the global hide toggle is OFF, NPCs should always stay visible.
if (isNpc) {
if (!config.hideNPCs) return true;
if (group == null) return false;
final boolean listed = group.containsPlayer(nameLower);
return group.isHideListedNpcs() ? !listed : listed;
}

if (group == null) return false; // no groups, hide mode: only whitelisted players are visible
final boolean listed = group.containsPlayer(nameLower);
return group.isHideListedPlayers() ? !listed : listed;
}

/** True if the nametag for this visible player should be drawn (per-group setting when in group view). */
public boolean shouldShowNametagFor(@NotNull String nameLower) {
if (!isInHidePlayersMode()) return true;
final PlayerGroup group = getCurrentViewGroup();
if (group == null) return true; // hide-all with no groups: use default
if (!group.containsPlayer(nameLower)) return true; // whitelist/NPC visibility: show nametag
return group.isShowNametags();
}

@Contract(pure = true)
public static int max(int... values) {
int max = Integer.MIN_VALUE;
Expand Down
Loading
Loading