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
129 changes: 71 additions & 58 deletions forge/origin/src/main/java/io/github/kituin/chatimage/ChatImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
import io.github.kituin.chatimage.network.FileInfoChannel;
import io.github.kituin.ChatImageCode.ChatImageCodeInstance;
import io.github.kituin.ChatImageCode.ChatImageConfig;
import io.github.kituin.ChatImageCode.ChatImageFrame;
import io.github.kituin.ChatImageCode.ClientStorage;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;

Expand All @@ -32,16 +35,10 @@
import org.apache.logging.log4j.Logger;

import java.io.File;
// IF > forge-1.18.2
//import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
// END IF
// IF forge-1.16.5
//import net.minecraftforge.fml.ExtensionPoint;
// ELSE
//import net.minecraft.commands.Commands;
//import net.minecraftforge.client.event.RegisterClientCommandsEvent;
//import net.minecraftforge.event.server.ServerStartingEvent;
// END IF
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraft.commands.Commands;
import net.minecraftforge.client.event.RegisterClientCommandsEvent;
import net.minecraftforge.event.server.ServerStartingEvent;

import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;

Expand Down Expand Up @@ -77,60 +74,76 @@ public static class ClientModEvents {
CONFIG = ChatImageConfig.loadConfig();
ChatImageCodeInstance.CLIENT_ADAPTER = new ChatImageClientAdapter();
}
// IF >= forge-1.19
// @SubscribeEvent
// public static void onKeyBindRegister(RegisterKeyMappingsEvent event) {
// KeyBindings.init(event);
// LOGGER.info("KeyBindings Register");
// }
// END IF

@SubscribeEvent
public static void onKeyBindRegister(RegisterKeyMappingsEvent event) {
KeyBindings.init(event);
LOGGER.info("KeyBindings Register");
}

@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
LOGGER.info("[ChatImage]Client start");
// IF >= forge-1.16 && < forge-1.19
// KeyBindings.init();
// LOGGER.info("KeyBindings Register");
// END IF
// IF forge-1.16.5
// ModLoadingContext.get().registerExtensionPoint(ExtensionPoint.CONFIGGUIFACTORY,
// () -> (mc, screen) -> new ConfigScreen(screen));
// ELSE
// ModLoadingContext.get().registerExtensionPoint(#ConfigScreenFactory#.class, () -> new #ConfigScreenFactory#((minecraft, screen) -> new ConfigScreen(screen)));
// MinecraftForge.EVENT_BUS.addListener(ClientModEvents::onClientCommand);
// END IF
ModLoadingContext.get().registerExtensionPoint(net.minecraftforge.client.ConfigScreenHandler.ConfigScreenFactory.class, () -> new net.minecraftforge.client.ConfigScreenHandler.ConfigScreenFactory((minecraft, screen) -> new ConfigScreen(screen)));
MinecraftForge.EVENT_BUS.addListener(ClientModEvents::onClientCommand);
MinecraftForge.EVENT_BUS.addListener(ClientModEvents::onKeyInput);
MinecraftForge.EVENT_BUS.addListener(ClientModEvents::onClientStaring);

// ========== 新增:注册GIF动画Tick更新 ==========
MinecraftForge.EVENT_BUS.addListener(ClientModEvents::onClientTick);
LOGGER.info("[ChatImage]GIF动画Tick已注册");
// ==============================================
}

// ========== 新增:客户端Tick事件,驱动GIF动画(使用反射访问ClientStorage私有字段) ==========
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase == TickEvent.Phase.END) {
try {
java.lang.reflect.Field imagesField = ClientStorage.class.getDeclaredField("images");
imagesField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.HashMap<String, ChatImageFrame> images =
(java.util.HashMap<String, ChatImageFrame>) imagesField.get(null);

for (ChatImageFrame frame : images.values()) {
if (frame != null && frame.getSiblings().size() > 0) {
frame.gifLoop(2);
}
}
} catch (Exception e) {
// 静默失败,不影响正常功能
}
}
}
// IF > forge-1.16.5
// public static void onClientCommand(RegisterClientCommandsEvent event) {
// CommandDispatcher<#CommandSourceStack#> dispatcher = event.getDispatcher();
// LiteralCommandNode<#CommandSourceStack#> cmd = dispatcher.register(
// Commands.literal(MOD_ID)
// .then(Commands.literal("send")
// .then(Commands.argument("name", StringArgumentType.string())
// .then(Commands.argument("url", greedyString())
// .executes(SendChatImage.instance)
// )
// )
// )
// .then(Commands.literal("url")
// .then(Commands.argument("url", greedyString())
// .executes(SendChatImage.instance)
// )
// )
// .then(Commands.literal("help")
// .executes(Help.instance)
// )
// .then(Commands.literal("reload")
// .executes(ReloadConfig.instance)
// )
//
// );
// }
// END IF

public static void onKeyInput(#InputEvent.Key# event) {
// ========================================================================================

public static void onClientCommand(RegisterClientCommandsEvent event) {
CommandDispatcher<net.minecraft.commands.CommandSourceStack> dispatcher = event.getDispatcher();
LiteralCommandNode<net.minecraft.commands.CommandSourceStack> cmd = dispatcher.register(
Commands.literal(MOD_ID)
.then(Commands.literal("send")
.then(Commands.argument("name", StringArgumentType.string())
.then(Commands.argument("url", greedyString())
.executes(SendChatImage.instance)
)
)
)
.then(Commands.literal("url")
.then(Commands.argument("url", greedyString())
.executes(SendChatImage.instance)
)
)
.then(Commands.literal("help")
.executes(Help.instance)
)
.then(Commands.literal("reload")
.executes(ReloadConfig.instance)
)

);
}

public static void onKeyInput(net.minecraftforge.client.event.InputEvent.Key event) {
if (KeyBindings.gatherManaKeyMapping.consumeClick()) {
Minecraft.getInstance().setScreen(new ConfigScreen(Minecraft.getInstance().screen));
}
Expand All @@ -139,4 +152,4 @@ public static void onKeyInput(#InputEvent.Key# event) {
public static void onClientStaring(RegisterCommandsEvent event) {
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -25,7 +26,10 @@ public int getTimeOut() {

@Override
public ChatImageFrame.TextureReader<#ResourceLocation#> loadTexture(InputStream image) throws IOException {
#NativeImage# nativeImage = #NativeImage#.read(image);
#NativeImage# nativeImage = loadNativeImage(image);
if (nativeImage == null) {
throw new IOException("Failed to load image into NativeImage");
}
return new ChatImageFrame.TextureReader<>(
Minecraft.getInstance().getTextureManager().register(MOD_ID + "/chatimage",
new DynamicTexture(nativeImage)),
Expand All @@ -34,6 +38,46 @@ public int getTimeOut() {
);
}

/**
* 兼容加载各种格式的图片为 NativeImage
*/
private static #NativeImage# loadNativeImage(InputStream image) throws IOException {
#NativeImage# nativeImage = null;

// 先尝试标准 NativeImage.read()
try {
nativeImage = #NativeImage#.read(image);
if (nativeImage != null) return nativeImage;
} catch (Exception ignored) {
}

// 失败时尝试用 ImageIO 中转转换
try {
image.reset();
java.awt.image.BufferedImage bufImage = javax.imageio.ImageIO.read(image);
if (bufImage == null) return null;

// 转换为标准 ARGB 格式
java.awt.image.BufferedImage rgbImage = new java.awt.image.BufferedImage(
bufImage.getWidth(), bufImage.getHeight(),
java.awt.image.BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics2D g = rgbImage.createGraphics();
g.drawImage(bufImage, 0, 0, null);
g.dispose();

// 写出为 PNG 再读入 NativeImage
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
if (javax.imageio.ImageIO.write(rgbImage, "png", baos)) {
nativeImage = #NativeImage#.read(
new ByteArrayInputStream(baos.toByteArray()));
}
} catch (Exception e) {
return nativeImage;
}

return nativeImage;
}

@Override
public void sendToServer(String url, File file, boolean isToServer) {
if (isToServer) {
Expand All @@ -58,7 +102,7 @@ public int getMaxFileSize() {
}

@Override
public #MutableComponent# getProcessMessage(int i) {
public net.minecraft.network.chat.MutableComponent getProcessMessage(int i) {
return createTranslatableComponent("process.chatimage.message", i);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import com.google.gson.Gson;
import io.github.kituin.ChatImageCode.ChatImageFrame;
import io.github.kituin.ChatImageCode.ChatImageIndex;
import io.github.kituin.ChatImageCode.NetworkHelper;
import net.minecraft.client.Minecraft;


import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -22,7 +22,6 @@ public class ChatImagePacket {

public static Gson gson = new Gson();


/**
* 发送给服务器一连串网络包(异步)
* 文件频道
Expand All @@ -44,17 +43,15 @@ public static void loadFromServer(String url) {
if (Minecraft.getInstance().player != null) {
FileInfoChannel.sendToServer(new FileInfoChannelPacket(url));
LOGGER.info("[try get from server]" + url);

} else {
AddImageError(url, ChatImageFrame.FrameError.FILE_NOT_FOUND);
}
}


/**
* 服务端接收 图片文件分块 的处理
*
* @param player #ServerPlayer#
* @param player net.minecraft.server.level.ServerPlayer
* @param res String
*/
public static void serverFileChannelReceived(#ServerPlayer# player, String res) {
Expand Down Expand Up @@ -90,7 +87,6 @@ public static void clientDownloadFileChannelReceived(String res) {
}
}


public static void clientFileInfoChannelReceived(String data) {
String url = data.substring(6);
LOGGER.info(url);
Expand All @@ -105,8 +101,19 @@ public static void clientFileInfoChannelReceived(String data) {

public static void serverFileInfoChannelReceived(#ServerPlayer# player, String url) {
HashMap<Integer, String> list = SERVER_BLOCK_CACHE.getBlock(url);

// ========== 修复:同时支持 file:// URI 和本地绝对路径 ==========
if (list == null) {
boolean isLocalPath = url.startsWith("file://")
|| (url.length() > 1 && url.charAt(1) == ':') // Windows: D:...
|| url.startsWith("/"); // Linux: /home/...
if (isLocalPath) {
list = loadLocalFileToCache(url);
}
}
// ============================================================

if (list != null) {
// 服务器存在缓存图片,直接发送给客户端
for (Map.Entry<Integer, String> entry : list.entrySet()) {
LOGGER.debug("[GetFileChannel->Client:{}/{}]{}", entry.getKey(), list.size() - 1, url);
DownloadFileChannel.sendToPlayer(new DownloadFileChannelPacket(entry.getValue()), player);
Expand All @@ -117,11 +124,56 @@ public static void serverFileInfoChannelReceived(#ServerPlayer# player, String u
// 通知客户端无文件
FileBackChannel.sendToPlayer(new FileInfoChannelPacket("null->" + url), player);
LOGGER.error("[GetFileChannel]not found in server:{}", url);
// 记录uuid,后续有文件了推送
if (player != null) {
SERVER_BLOCK_CACHE.tryAddUser(url, player.getStringUUID());
}
LOGGER.info("[GetFileChannel]记录uuid:{}", player.getStringUUID());
LOGGER.info("[not found in server]{}", url);
}
}

/**
* 读取本地文件(支持 file:// URI 和纯本地路径),使用 ChatImage 标准分包逻辑塞入 ServerBlockCache
*/
private static HashMap<Integer, String> loadLocalFileToCache(String fileUrl) {
try {
java.io.File file;

// 区分 file:// URI 和纯本地路径
if (fileUrl.startsWith("file://")) {
java.net.URI uri = new java.net.URI(fileUrl);
java.nio.file.Path path = java.nio.file.Paths.get(uri);
file = path.toFile();
} else {
file = new java.io.File(fileUrl);
}

if (!file.exists()) {
LOGGER.error("[FileFallback]本地文件不存在:{}", file.getAbsolutePath());
return null;
}

// 使用 NetworkHelper 的标准分包逻辑(自动处理大文件分包)
List<String> packets = NetworkHelper.createFilePacket(fileUrl, file);
if (packets.isEmpty()) {
LOGGER.error("[FileFallback]文件分包失败:{}", fileUrl);
return null;
}

// 将分包塞入 ServerBlockCache
for (String packet : packets) {
ChatImageIndex index = gson.fromJson(packet, ChatImageIndex.class);
SERVER_BLOCK_CACHE.createBlock(index, packet);
}

LOGGER.info("[FileFallback]本地文件已加载到缓存:{} ({} bytes, {} packets)",
fileUrl, file.length(), packets.size());

// 返回完整的块列表
return SERVER_BLOCK_CACHE.getBlock(fileUrl);

} catch (Exception e) {
LOGGER.error("[FileFallback]加载本地文件失败:{} - {}", fileUrl, e.getLocalizedMessage());
return null;
}
}
}