前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Webmapview:一个我的世界内置网页地图浏览Fabric模组

Webmapview:一个我的世界内置网页地图浏览Fabric模组

作者头像
姓王者
发布2025-02-28 17:46:34
发布2025-02-28 17:46:34
6700
代码可运行
举报
文章被收录于专栏:姓王者的博客
运行总次数:0
代码可运行

webmapview

xingwangzhe/webmapview: WebmapView allows Minecraft players to view web map services (squaremap) through an in-game browser interface, supporting features like custom URLs

ℹ️

本模组遵循GPL3.0 MIT协议

GPL3.0协议优先

为什么要开发这一个模组

因为我懒得在浏览器打开页面

挑战:我的世界并不能原生渲染web

这是正常的,查资料,我的世界基于opengl,但网页一般是webgl。这就意味着,我既不能用minecraft原生方法来写,也不可能用fabric api或者其他模组api来写。难道就此止步了吗?不,既然我不会造轮子,那我只好找轮子了,经历一番搜索,我终于找到了一个合适的依赖 [MCEF]Minecraft Chromium嵌入式框架 (Minecraft Chromium Embedded Framework) - MC百科|最大的Minecraft中文MOD百科

稍微改造轮子,实现我想要的效果

由于这个项目有个示例模组,已经实现了web对象,那么我只需要替换url就行了

CinemaMod/mcef-fabric-example-mod: Example MCEF Fabric mod

很高兴作者用了CC0协议。虽然表明已经投入公共领域,但我还是注明一下来源

改造后的BasicBrowser.java
代码语言:javascript
代码运行次数:0
复制
//copy from https://github.com/CinemaMod/mcef-fabric-example-mod and change by xingwangzhe

public class BasicBrowser extends Screen {
    private static final int BROWSER_DRAW_OFFSET = 20;

    private MCEFBrowser browser;

    private final MinecraftClient minecraft = MinecraftClient.getInstance();

    public BasicBrowser(Text title) {
        super(title);
    }

    @Override
    protected void init() {
        super.init();
        if (browser == null) {
            String url = UrlManager.fullUrl(UrlManager.defaultUrl);
            sendFeedback(url);
            boolean transparent = true;
            browser = MCEF.createBrowser(url, transparent);
            resizeBrowser();
        }
    }

下面就得解释一下关键的方法

UrlManager对象

关键在于实现对url的管理

代码语言:javascript
代码运行次数:0
复制
public class UrlManager {
    private static final String URL_FILE_NAME = "urls.txt"; // 存储URL列表的文件名
    private static final String DEFAULT_URL_FILE_NAME = "default_url.txt"; // 默认URL文件名
    private static List<String> urlList = new ArrayList<>(); // 存储URL的列表
    public static String defaultUrl="squaremap-demo.jpenilla.xyz"; // 默认URL
    public static boolean webmapview=true;
    static {
        loadUrls();
        loadDefaultUrl();
    }

    /**
     * 添加一个URL到列表中并保存到文件。
     * @param url 要添加的URL字符串
     */
    public static void addUrl(String url) {
        if (!urlList.contains(url)) { // 确保不重复添加相同的URL
            urlList.add(url);
            saveUrls(); // 保存更新后的URL列表到文件
            sendFeedback(Text.translatable("feedback.url.added", url)); // 向玩家发送反馈
        } else {
            sendFeedback(Text.translatable("feedback.url.exists", url)); // 如果URL已存在,则通知玩家
        }
    }

    /**
     * 设置默认URL。
     * @param url 要设置为默认的URL字符串
     */
    public static void setDefaultUrl(String url) {
        if (urlList.contains(url)) { // 确保URL已经存在于列表中
            defaultUrl = url;
            saveDefaultUrl(); // 更新默认URL到文件
            sendFeedback(Text.translatable("feedback.default.url.updated", url)); // 向玩家发送反馈
        } else {
            sendFeedback(Text.translatable("feedback.url.not_found", url)); // 如果URL不存在于列表中,则通知玩家
        }
    }

    /**
     * 获取当前的URL列表。
     * @return 包含所有URL的列表
     */
    public static List<String> getUrlList() {
        return urlList;
    }

    /**
     * 获取默认URL。
     * @return 默认URL字符串
     */
    public static String getDefaultUrl() {
        return defaultUrl;
    }

    /**
     * 将当前的URL列表保存到文件中。
     */
    private static void saveUrls() {
        Path configPath = getConfigDirectory().resolve(URL_FILE_NAME); // 获取配置文件路径
        try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { // 使用try-with-resources确保资源关闭
            for (String url : urlList) {
                writer.write(url); // 写入单个URL
                writer.newLine(); // 换行以便每个URL占一行
            }
        } catch (IOException e) {
            e.printStackTrace(); // 打印异常信息
        }
    }

    /**
     * 从文件加载URL列表。
     */
    private static void loadUrls() {
        Path configPath = getConfigDirectory().resolve(URL_FILE_NAME); // 获取配置文件路径
        urlList.clear(); // 清空现有列表以准备加载新数据
        if (Files.exists(configPath)) { // 如果文件存在,则读取它
            try (BufferedReader reader = Files.newBufferedReader(configPath)) { // 使用try-with-resources确保资源关闭
                String line;
                while ((line = reader.readLine()) != null) { // 循环读取每一行
                    urlList.add(line); // 将每行作为一个URL添加到列表中
                }
            } catch (IOException e) {
                e.printStackTrace(); // 打印异常信息
            }
        }
    }

    /**
     * 保存默认URL到文件。
     */
    private static void saveDefaultUrl() {
        Path defaultUrlPath = getConfigDirectory().resolve(DEFAULT_URL_FILE_NAME); // 获取默认URL文件路径
        try (BufferedWriter writer = Files.newBufferedWriter(defaultUrlPath)) { // 使用try-with-resources确保资源关闭
            if (defaultUrl != null && !defaultUrl.trim().isEmpty()) {
                writer.write(defaultUrl); // 写入默认URL
            } else {
                // 如果没有有效的默认URL,则删除默认URL文件(如果存在)
                Files.deleteIfExists(defaultUrlPath);
            }
        } catch (IOException e) {
            e.printStackTrace(); // 打印异常信息
        }
    }

    /**
     * 从文件加载默认URL。
     */
    private static void loadDefaultUrl() {
        Path defaultUrlPath = getConfigDirectory().resolve(DEFAULT_URL_FILE_NAME); // 获取默认URL文件路径
        if (Files.exists(defaultUrlPath)) { // 如果文件存在,则读取它
            try (BufferedReader reader = Files.newBufferedReader(defaultUrlPath)) { // 使用try-with-resources确保资源关闭
                String line;
                if ((line = reader.readLine()) != null) { // 只读取第一行
                    defaultUrl = line; // 设置默认URL
                }
            } catch (IOException e) {
                e.printStackTrace(); // 打印异常信息
            }
        }
    }

    /**
     * 获取配置目录路径。
     * @return Minecraft配置目录下的路径
     */
    private static Path getConfigDirectory() {
        Path configDir = FabricLoader.getInstance().getConfigDir();
        try {
            Files.createDirectories(configDir); // 如果目录不存在,则创建之
        } catch (IOException e) {
            e.printStackTrace();
        }
        return configDir;
    }

    /**
     * 发送反馈信息到聊天栏。
     * @param message 要发送的消息
     */
    public static void sendFeedback(String message) {
        MinecraftClient.getInstance().player.sendMessage(Text.of(message), false);
    }
    public static void sendFeedback(Text textMessage) {
        if (MinecraftClient.getInstance().player != null) {
            MinecraftClient.getInstance().player.sendMessage(textMessage, false);
        }
    }
    public static void removeUrl(String url) {
        if (urlList.remove(url)) { // 如果成功移除URL
            saveUrls(); // 保存更新后的URL列表到文件

            // 如果被删除的URL是默认URL,则清除默认URL设置
            if (defaultUrl != null && defaultUrl.equals(url)) {
                clearDefaultUrl();
            }

            sendFeedback(Text.translatable("feedback.url.removed", url)); // 向玩家发送反馈
        } else {
            sendFeedback(Text.translatable("feedback.url.not_found", url)); // 如果URL不存在,则通知玩家
        }
    }

    /**
     * 清除默认URL设置。
     */
    private static void clearDefaultUrl() {
        defaultUrl = null;
        Path defaultUrlPath = getConfigDirectory().resolve(DEFAULT_URL_FILE_NAME); // 获取默认URL文件路径
        try {
            Files.deleteIfExists(defaultUrlPath); // 删除默认URL文件(如果存在)
        } catch (IOException e) {
            e.printStackTrace(); // 打印异常信息
        }
    }

    public static String fullUrl(String baseUrl) {
        MinecraftClient client = MinecraftClient.getInstance();
        if (client.player == null || client.world == null|| webmapview==false ) {
            sendFeedback(Text.translatable("feedback.player_or_world_not_available")); // 使用翻译文本
            return baseUrl; // 如果无法获取玩家或世界信息,则返回原始URL
        }

        // 获取玩家坐标并转换为整数
        int playerX = (int) client.player.getX();
        int playerZ = (int) client.player.getZ();

        // 获取玩家所在的世界名称
        String worldName = client.world.getRegistryKey().getValue().toString();
        if(Objects.equals(worldName, "minecraft:overworld")){
            worldName = "world";
        } else if (Objects.equals(worldName, "minecraft:the_nether")){
            worldName = "world_nether";
        } else if (Objects.equals(worldName, "minecraft:the_end")){
            worldName = "world_the_end";
        }

        // 构建完整的URL
        StringBuilder fullUrlBuilder = new StringBuilder("https://").append(baseUrl).append("/"); // 添加协议头
        if (!baseUrl.contains("?")) { // 检查是否已有参数
            fullUrlBuilder.append("?");
        } else {
            fullUrlBuilder.append("&"); // 如果已经有参数,则使用&连接
        }
        fullUrlBuilder.append("x=").append(playerX)
                .append("&z=").append(playerZ)
                .append("&zoom=").append("4")
                .append("&world=").append(worldName);

        return fullUrlBuilder.toString();
    }
}

初始化,注册命令绑定按键

代码语言:javascript
代码运行次数:0
复制
public class WebmapviewClient implements ClientModInitializer {

    private static CompletableFuture<Suggestions> suggestUrls(CommandContext<?> context, SuggestionsBuilder builder) {
        List<String> urls = UrlManager.getUrlList();
        urls.forEach(builder::suggest);
        return builder.buildFuture();
    }
    private KeyBinding keyBinding;
    @Override
    public void onInitializeClient() {
//        ClientTickEvents.START_CLIENT_TICK.register((client) -> onTick());
        // 注册“addturl”命令,用于添加新的URL
        ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
            dispatcher.register(
                    ClientCommandManager.literal("urladd")
                            .then(ClientCommandManager.argument("url", StringArgumentType.string())
                                    .executes(context -> {
                                        String url = StringArgumentType.getString(context, "url");
                                        UrlManager.addUrl(url); // 添加URL
                                        return 1; // 命令成功执行
                                    })
                            )
            );

            // 注册“removeurl”命令,用于删除URL
            dispatcher.register(
                    ClientCommandManager.literal("urlremove")
                            .then(ClientCommandManager.argument("url", StringArgumentType.string()).suggests(WebmapviewClient::suggestUrls)
                                    .executes(context -> {
                                        String url = StringArgumentType.getString(context, "url");
                                        UrlManager.removeUrl(url); // 删除URL
                                        return 1; // 命令成功执行
                                    })
                            )
            );

            // 注册“urllist”命令,用于列出所有已添加的URL
            dispatcher.register(
                    ClientCommandManager.literal("urllist")
                            .executes(context -> {
                                List<String> urls = UrlManager.getUrlList(); // 获取所有URL
                                StringBuilder listMessage = new StringBuilder("Available URLs:\n");
                                for (int i = 0; i < urls.size(); i++) {
                                    listMessage.append(i + 1).append(": ").append(urls.get(i)).append("\n"); // 格式化输出
                                }
                                context.getSource().sendFeedback(Text.of(listMessage.toString())); // 向玩家展示结果
                                return 1; // 命令成功执行
                            })
            );
            // 注册“urlset”命令,用于设置默认URL
            dispatcher.register(
                    ClientCommandManager.literal("urlset")
                            .then(ClientCommandManager.argument("url", StringArgumentType.string()).suggests(WebmapviewClient::suggestUrls)
                                    .executes(context -> {
                                        String url = StringArgumentType.getString(context, "url");
                                        UrlManager.setDefaultUrl(url); // 设置默认URL
                                        return 1; // 命令成功执行
                                    })
                            )
            );
            dispatcher.register(
                    ClientCommandManager.literal("webmapviewoption")
                            .then(ClientCommandManager.argument("url", StringArgumentType.string())
                                    .executes(context -> {
                                        UrlManager.webmapview = !UrlManager.webmapview;
                                        if (UrlManager.webmapview) {
                                            sendFeedback("webmapview is enabled");
                                        } else {
                                            sendFeedback("webmapview is not enabled");
                                        }

                                   return 1; })
                            )
            );
            dispatcher.register(
                    ClientCommandManager.literal("webmapview")
                            .then(ClientCommandManager.literal("help")
                                    .executes(context -> {
                                        StringBuilder helpMessage = new StringBuilder();
                                        helpMessage.append("/urladd: ").append(Text.translatable("command.urladd.description").getString()).append("\n")
                                                .append("/urlremove: ").append(Text.translatable("command.urlremove.description").getString()).append("\n")
                                                .append("/urllist: ").append(Text.translatable("command.urllist.description").getString()).append("\n")
                                                .append("/urlset: ").append(Text.translatable("command.urlset.description").getString()).append("\n")
                                                .append("/webmapviewoption: ").append(Text.translatable("command.webmapviewoption.description").getString()).append("\n");
                                        ;
                                        sendFeedback((helpMessage.toString()) );
                                        return 1;
                                    })
                            )
            );
        });

        // 初始化KeyBinding
        keyBinding = new KeyBinding(
                "key.webmapview.open_basic_browser",  // 使用唯一标识符
                GLFW.GLFW_KEY_H,                      // 默认按键
                "category.webmapview"                       // 分类
        );

        // 注册KeyBinding
        KeyBindingHelper.registerKeyBinding(keyBinding);

        final MinecraftClient minecraft = MinecraftClient.getInstance();
        // 监听客户端tick事件,处理按键输入
        ClientTickEvents.END_CLIENT_TICK.register(client -> {
            while (keyBinding.wasPressed()) {
                if (!(minecraft.currentScreen instanceof BasicBrowser)) {
                    minecraft.setScreen(new BasicBrowser(
                            Text.literal("Basic Browser")
                    ));
                }
            }
        });
    }
    }

感悟

困难也重重

由于我是计算机专业的,虽然我没系统性地学习java,但条件控制语句,oop什么的基本上还是会的。但关键问题在于

事件过程应该是什么

就拿检测是否按下按键来说吧,我以为用if就行,没想到需要使用while()来实现监听,光这一点就卡了我很长时间,

我可算知道为什么模组一多就占内存了,事件占用太多了😀。

显然,我不能简单地想象时间的流程,不然我找不到对应地api。很多模组教学没有提到这一点,他们只会一味地提及去查wiki,但问题在于我的想法太简化/复杂, 根本找不全应有的的函数方法。

调试再调试,报错再报错

每一次改代码,都需要重载,虽然耗时,但无可避免。调试的时候,依赖依赖找不到,有时候莫名其妙还得重新构建一下,历史信息太过沉重,以至于找到很多废弃的api 😦

在我尝试mcef之前,使用的非我的世界相关依赖更是,一言难尽… 只能说我确实不懂java工程

想打印信息,发现好多都可以打印,有点感觉到回字的四种写法

收获亦颇丰

了解到了一些概念

比如说数据持久化,我需要把urls放在txt里面,这样下次打开游戏可以直接用,如果只是写在内存里,关了游戏,数据自然就消失了

代码语言:javascript
代码运行次数:0
复制
private static final String URL_FILE_NAME = "urls.txt"; // 存储URL列表的文件名
    private static final String DEFAULT_URL_FILE_NAME = "default_url.txt"; // 默认URL文件名
    private static List<String> urlList = new ArrayList<>(); // 存储URL的列表
    public static String defaultUrl="squaremap-demo.jpenilla.xyz"; // 默认URL
    public static boolean webmapview=true;
工程规范

稍微了解了一下gradle,ai还提了一下maven,默认src是资源,规定i18n要写在lang文件夹下等等。学了这些,我想我不只能看懂自己的模组工程,也能看懂别人的

java

是的,亲手敲代码确实能锻炼java功底,说不定毕业我就是拥有三年工作经验的jvav工程师了😂

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • webmapview
    • 为什么要开发这一个模组
    • 挑战:我的世界并不能原生渲染web
    • 稍微改造轮子,实现我想要的效果
      • 改造后的BasicBrowser.java
    • UrlManager对象
    • 初始化,注册命令绑定按键
  • 感悟
    • 困难也重重
      • 事件过程应该是什么
      • 调试再调试,报错再报错
    • 收获亦颇丰
      • 了解到了一些概念
      • 工程规范
      • java
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档