diff --git a/gradle.properties b/gradle.properties index bf96b0c..4576442 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ loom_version=1.11-SNAPSHOT # Mod Properties mod_version=1.0.0 -maven_group=com.example +maven_group=dev.astatin3.favicon archives_base_name=modid # Dependencies diff --git a/src/main/java/com/example/ExampleMod.java b/src/main/java/com/example/ExampleMod.java deleted file mode 100644 index 215cfbb..0000000 --- a/src/main/java/com/example/ExampleMod.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example; - -import net.fabricmc.api.ModInitializer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ExampleMod implements ModInitializer { - public static final String MOD_ID = "modid"; - - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - @Override - public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - LOGGER.info("Hello Fabric world!"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/mixin/ExampleMixin.java b/src/main/java/com/example/mixin/ExampleMixin.java deleted file mode 100644 index 3c4212c..0000000 --- a/src/main/java/com/example/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftServer.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "loadWorld") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftServer.loadWorld()V - } -} \ No newline at end of file diff --git a/src/main/java/dev/astatin3/favicon/ExampleMod.java b/src/main/java/dev/astatin3/favicon/ExampleMod.java new file mode 100644 index 0000000..b1f0eda --- /dev/null +++ b/src/main/java/dev/astatin3/favicon/ExampleMod.java @@ -0,0 +1,333 @@ +package dev.astatin3.favicon; + +import com.mojang.brigadier.arguments.StringArgumentType; +import net.fabricmc.api.ModInitializer; + +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.argument.TextArgumentType; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.MapIdComponent; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.map.MapState; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerMetadata; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +import static net.minecraft.server.command.CommandManager.literal; + +public class ExampleMod implements ModInitializer { + public static final String MOD_ID = "favicon"; + + // This logger is used to write text to the console and the log file. + // It is considered best practice to use your mod id as the logger's name. + // That way, it's clear which mod wrote info, warnings, and errors. + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + @Override + public void onInitialize() { + // This code runs as soon as Minecraft is in a mod-load-ready state. + // However, some things (like resources) may still be uninitialized. + // Proceed with mild caution. + +// LOGGER.info("Hello Fabric world!"); + + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("favicon") + .executes(context -> { + // For versions below 1.19, replace "Text.literal" with "new LiteralText". + // For versions below 1.20, remode "() ->" directly. + + ServerPlayerEntity player = context.getSource().getPlayer(); + ItemStack mainHandStack = player.getMainHandStack(); + if(mainHandStack == null) { + context.getSource().sendFeedback(() -> Text.literal("Error getting held item"), false); + return 1; + } + Item item = mainHandStack.getItem(); + if(!Objects.equals(item.toString(), "minecraft:filled_map")) { + context.getSource().sendFeedback(() -> Text.literal("Invalid held item. You must be holding a Filled Map"), false); + return 1; + } + + + MapIdComponent mapIdComponent = mainHandStack.get(DataComponentTypes.MAP_ID); + MapState mapState = player.getWorld().getMapState(mapIdComponent); + + if(mapState == null) { + context.getSource().sendFeedback(() -> Text.literal("Error getting map data"), false); + return 1; + } + + MinecraftServer server = player.getServer(); + + ServerMetadata metadata = server.getServerMetadata(); + + if(metadata == null) { + context.getSource().sendFeedback(() -> Text.literal("Error getting server metadata"), false); + return 1; + } + + context.getSource().sendFeedback(() -> Text.literal("Encoding icon data..."), false); + + +// for (int y = 0; y < 64; y++) { +// String text = ""; +// for (int x = 0; x < 64; x++) { +// text += mapState.colors[y * 128 + x] + ", "; +// } +// String finalText = text; +// context.getSource().sendFeedback(() -> Text.literal(finalText), false); +// } + + BufferedImage bufImg = convertImage(mapState.colors); + byte[] favicon = toBytes(bufImg); + + if(favicon == null) { + context.getSource().sendFeedback(() -> Text.literal("Error encoding icon"), false); + return 1; + } + + context.getSource().sendFeedback(() -> Text.literal("Setting current server icon..."), false); + + ServerMetadata newMetadata = new ServerMetadata( + metadata.description(), + metadata.players(), + metadata.version(), + Optional.of(new ServerMetadata.Favicon(favicon)), + metadata.secureChatEnforced() + ); + + ((MetadataAccessor) server).setServerMetadata(newMetadata); + + context.getSource().sendFeedback(() -> Text.literal("Saving new icon file..."), false); + + Path iconpath = server.getPath("server-icon.png"); + try { + FileUtils.writeByteArrayToFile(iconpath.toFile(), favicon); + } catch (IOException e) { + throw new RuntimeException(e); + } + + context.getSource().sendFeedback(() -> Text.literal("Done!"), false); + + context.getSource().sendFeedback(() -> Text.literal("Current icon: " + server.getServerMetadata().favicon().get().iconBytes().length + " bytes"), false); + + + return 1; + }))); + + + +// CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register(literal("motd") +// .then(CommandManager.argument("MOTD", StringArgumentType.greedyString()) +// .executes(context -> { +// +// ServerPlayerEntity player = context.getSource().getPlayer(); +// MinecraftServer server = player.getServer(); +// ServerMetadata metadata = server.getServerMetadata(); +// +// if(metadata == null) { +// context.getSource().sendFeedback(() -> Text.literal("Error getting server metadata"), false); +// return 1; +// } +// +// context.getSource().sendFeedback(() -> Text.literal("Setting server metadata..."), false); +// context.getSource().sendFeedback(() -> Text.literal("Preview:"), false); +// +// String motdText = StringArgumentType.getString(context, "MOTD"); +// Text motd = Text.of(motdText); +// +// context.getSource().sendFeedback(() -> motd, false); +// +// ServerMetadata newMetadata = new ServerMetadata( +// motd, +// metadata.players(), +// metadata.version(), +// metadata.favicon(), +// metadata.secureChatEnforced() +// ); +// +// ((MetadataAccessor) server).setServerMetadata(newMetadata); +// +// +// return 1; +// })))); + } + + private BufferedImage convertImage(byte[] mapBytes) { + + if (mapBytes.length != 16384) { + throw new IllegalArgumentException("Image data must be exactly 16384 bytes (128x128)"); + } + + // Create cropped image data (64x64 = 4096 bytes) +// byte[] croppedData = new byte[64 * 64]; + + BufferedImage bufferedImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB); + + // Copy top-left 64x64 quadrant + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + bufferedImage.setRGB(x, y, mapByteToColor(mapBytes[y * 128 + x])); + } + } + + return bufferedImage; + } + + private byte[] toBytes(BufferedImage bufferedImage) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + ImageIO.write(bufferedImage, "PNG", baos); + } catch (Exception e) { + return null; + } + + return baos.toByteArray(); + } + + // https://minecraft.wiki/w/Map_item_format + private static final int[][] BASE_COLORS = { + {0, 0, 0}, // 0: None/Transparent + {127, 178, 56}, // 1: Grass + {247, 233, 163}, // 2: Sand + {199, 199, 199}, // 3: Wool + {255, 0, 0}, // 4: Fire + {160, 160, 255}, // 5: Ice + {167, 167, 167}, // 6: Metal + {0, 124, 0}, // 7: Plant + {255, 255, 255}, // 8: Snow + {164, 168, 184}, // 9: Clay + {151, 109, 77}, // 10: Dirt + {112, 112, 112}, // 11: Stone + {64, 64, 255}, // 12: Water + {143, 119, 72}, // 13: Wood + {255, 252, 245}, // 14: Quartz + {216, 127, 51}, // 15: Color Orange + {178, 76, 216}, // 16: Color Magenta + {102, 153, 216}, // 17: Color Light Blue + {229, 229, 51}, // 18: Color Yellow + {127, 204, 25}, // 19: Color Light Green + {242, 127, 165}, // 20: Color Pink + {76, 76, 76}, // 21: Color Gray + {153, 153, 153}, // 22: Color Light Gray + {76, 127, 153}, // 23: Color Cyan + {127, 63, 178}, // 24: Color Purple + {51, 76, 178}, // 25: Color Blue + {102, 76, 51}, // 26: Color Brown + {102, 127, 51}, // 27: Color Green + {153, 51, 51}, // 28: Color Red + {25, 25, 25}, // 29: Color Black + {250, 238, 77}, // 30: Gold + {92, 219, 213}, // 31: Diamond + {74, 128, 255}, // 32: Lapis + {0, 217, 58}, // 33: Emerald + {129, 86, 49}, // 34: Podzol + {112, 2, 0}, // 35: Nether + {209, 177, 161}, // 36: Terracotta White + {159, 82, 36}, // 37: Terracotta Orange + {149, 87, 108}, // 38: Terracotta Magenta + {112, 108, 138}, // 39: Terracotta Light Blue + {186, 133, 36}, // 40: Terracotta Yellow + {103, 117, 53}, // 41: Terracotta Light Green + {160, 77, 78}, // 42: Terracotta Pink + {57, 41, 35}, // 43: Terracotta Gray + {135, 107, 98}, // 44: Terracotta Light Gray + {87, 92, 92}, // 45: Terracotta Cyan + {122, 73, 88}, // 46: Terracotta Purple + {76, 62, 92}, // 47: Terracotta Blue + {76, 50, 35}, // 48: Terracotta Brown + {76, 82, 42}, // 49: Terracotta Green + {142, 60, 46}, // 50: Terracotta Red + {37, 22, 16}, // 51: Terracotta Black + {189, 48, 49}, // 52: Crimson Nylium + {148, 63, 97}, // 53: Crimson Stem + {92, 25, 29}, // 54: Crimson Hyphae + {22, 126, 134}, // 55: Warped Nylium + {58, 142, 140}, // 56: Warped Stem + {86, 44, 62}, // 57: Warped Hyphae + {20, 180, 133}, // 58: Warped Wart Block + {100, 100, 100}, // 59: Deepslate + {216, 175, 147}, // 60: Raw Iron + {127, 167, 150} // 61: Glow Lichen + }; + + // Brightness multipliers for each shade level + private static final double[] BRIGHTNESS_MULTIPLIERS = { + 180.0 / 255.0, // 0: Darkest + 220.0 / 255.0, // 1: Dark + 255.0 / 255.0, // 2: Normal (brightest) + 135.0 / 255.0 // 3: Very dark + }; + + public static int mapByteToColor(byte mapByte) { + int unsignedByte = mapByte & 0xFF; + + // Color ID 0 is transparent + if (unsignedByte == 0) { + return 0; + } + + // Decode base color and brightness level + int baseColorIndex = (unsignedByte - 1) / 4; + int brightnessLevel = (unsignedByte - 1) % 4; + + // Validate base color index + if (baseColorIndex < 0 || baseColorIndex >= BASE_COLORS.length) { + return 0; // Return transparent for invalid colors + } + // Get base color RGB values + int[] baseRgb = BASE_COLORS[baseColorIndex]; + double multiplier = BRIGHTNESS_MULTIPLIERS[brightnessLevel]; + + // Apply brightness multiplier + int r = (int) Math.round(baseRgb[0] * multiplier); + int g = (int) Math.round(baseRgb[1] * multiplier); + int b = (int) Math.round(baseRgb[2] * multiplier); + + // Clamp values to 0-255 range + r = Math.max(0, Math.min(255, r)); + g = Math.max(0, Math.min(255, g)); + b = Math.max(0, Math.min(255, b)); + + // Combine into single RGB integer + return (r << 16) | (g << 8) | b; + } + +// public static int convertRgbByteToInt(byte rgbByte) { +// // Convert byte to unsigned int (0-255 range) +// int unsignedByte = rgbByte & 0xFF; +// +// // Extract the color components using bit masks and shifts +// int r = (unsignedByte >> 5) & 0x07; // Get top 3 bits (RRR) +// int g = (unsignedByte >> 2) & 0x07; // Get middle 3 bits (GGG) +// int b = unsignedByte & 0x03; // Get bottom 2 bits (BB) +// +// // Scale the values to full 8-bit range (0-255) +// // 3 bits (0-7) -> 8 bits (0-255): multiply by 255/7 ≈ 36.43 +// // 2 bits (0-3) -> 8 bits (0-255): multiply by 255/3 = 85 +// r = (r * 255) / 7; // Scale 3-bit to 8-bit +// g = (g * 255) / 7; // Scale 3-bit to 8-bit +// b = (b * 255) / 3; // Scale 2-bit to 8-bit +// +// // Combine into ARGB format: 0xAARRGGBB +// // Alpha = 255 (0xFF), fully opaque +// int argb = (255 << 24) | (r << 16) | (g << 8) | b; +// +// return argb; +// } +} \ No newline at end of file diff --git a/src/main/java/dev/astatin3/favicon/MetadataAccessor.java b/src/main/java/dev/astatin3/favicon/MetadataAccessor.java new file mode 100644 index 0000000..3745fc7 --- /dev/null +++ b/src/main/java/dev/astatin3/favicon/MetadataAccessor.java @@ -0,0 +1,10 @@ +package dev.astatin3.favicon; + +import net.minecraft.server.ServerMetadata; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public interface MetadataAccessor { + default void setServerMetadata(ServerMetadata metadata) {} +} diff --git a/src/main/java/dev/astatin3/favicon/mixin/ExampleMixin.java b/src/main/java/dev/astatin3/favicon/mixin/ExampleMixin.java new file mode 100644 index 0000000..b72c365 --- /dev/null +++ b/src/main/java/dev/astatin3/favicon/mixin/ExampleMixin.java @@ -0,0 +1,22 @@ +package dev.astatin3.favicon.mixin; + +import dev.astatin3.favicon.MetadataAccessor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerMetadata; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; + +@Mixin(MinecraftServer.class) +public class ExampleMixin implements MetadataAccessor { + @Shadow @Nullable private ServerMetadata metadata; + @Shadow @Nullable private ServerMetadata.Favicon favicon; + + @Override + public void setServerMetadata(ServerMetadata metadata) { + this.metadata = metadata; + this.favicon = metadata.favicon().orElse(null); + } +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 94f428b..f1c6d17 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -5,7 +5,7 @@ "name": "Example mod", "description": "This is an example description! Tell everyone what your mod is about!", "authors": [ - "Me!" + "Astatin3" ], "contact": { "homepage": "https://fabricmc.net/", @@ -16,18 +16,11 @@ "environment": "*", "entrypoints": { "main": [ - "com.example.ExampleMod" - ], - "client": [ - "com.example.ExampleModClient" + "dev.astatin3.favicon.ExampleMod" ] }, "mixins": [ - "modid.mixins.json", - { - "config": "modid.client.mixins.json", - "environment": "client" - } + "modid.mixins.json" ], "depends": { "fabricloader": ">=0.16.14", diff --git a/src/main/resources/modid.mixins.json b/src/main/resources/modid.mixins.json index f48035e..bd69e19 100644 --- a/src/main/resources/modid.mixins.json +++ b/src/main/resources/modid.mixins.json @@ -1,9 +1,9 @@ { "required": true, - "package": "com.example.mixin", + "package": "dev.astatin3.favicon.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ - "ExampleMixin" + "ExampleMixin" ], "injectors": { "defaultRequire": 1