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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
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的管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
| public class UrlManager { private static final String URL_FILE_NAME = "urls.txt"; private static final String DEFAULT_URL_FILE_NAME = "default_url.txt"; private static List<String> urlList = new ArrayList<>(); public static String defaultUrl="squaremap-demo.jpenilla.xyz"; public static boolean webmapview=true; static { loadUrls(); loadDefaultUrl(); }
public static void addUrl(String url) { if (!urlList.contains(url)) { urlList.add(url); saveUrls(); sendFeedback(Text.translatable("feedback.url.added", url)); } else { sendFeedback(Text.translatable("feedback.url.exists", url)); } }
public static void setDefaultUrl(String url) { if (urlList.contains(url)) { defaultUrl = url; saveDefaultUrl(); sendFeedback(Text.translatable("feedback.default.url.updated", url)); } else { sendFeedback(Text.translatable("feedback.url.not_found", url)); } }
public static List<String> getUrlList() { return urlList; }
public static String getDefaultUrl() { return defaultUrl; }
private static void saveUrls() { Path configPath = getConfigDirectory().resolve(URL_FILE_NAME); try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { for (String url : urlList) { writer.write(url); writer.newLine(); } } catch (IOException e) { e.printStackTrace(); } }
private static void loadUrls() { Path configPath = getConfigDirectory().resolve(URL_FILE_NAME); urlList.clear(); if (Files.exists(configPath)) { try (BufferedReader reader = Files.newBufferedReader(configPath)) { String line; while ((line = reader.readLine()) != null) { urlList.add(line); } } catch (IOException e) { e.printStackTrace(); } } }
private static void saveDefaultUrl() { Path defaultUrlPath = getConfigDirectory().resolve(DEFAULT_URL_FILE_NAME); try (BufferedWriter writer = Files.newBufferedWriter(defaultUrlPath)) { if (defaultUrl != null && !defaultUrl.trim().isEmpty()) { writer.write(defaultUrl); } else { Files.deleteIfExists(defaultUrlPath); } } catch (IOException e) { e.printStackTrace(); } }
private static void loadDefaultUrl() { Path defaultUrlPath = getConfigDirectory().resolve(DEFAULT_URL_FILE_NAME); if (Files.exists(defaultUrlPath)) { try (BufferedReader reader = Files.newBufferedReader(defaultUrlPath)) { String line; if ((line = reader.readLine()) != null) { defaultUrl = line; } } catch (IOException e) { e.printStackTrace(); } } }
private static Path getConfigDirectory() { Path configDir = FabricLoader.getInstance().getConfigDir(); try { Files.createDirectories(configDir); } catch (IOException e) { e.printStackTrace(); } return configDir; }
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)) { saveUrls();
if (defaultUrl != null && defaultUrl.equals(url)) { clearDefaultUrl(); }
sendFeedback(Text.translatable("feedback.url.removed", url)); } else { sendFeedback(Text.translatable("feedback.url.not_found", url)); } }
private static void clearDefaultUrl() { defaultUrl = null; Path defaultUrlPath = getConfigDirectory().resolve(DEFAULT_URL_FILE_NAME); try { Files.deleteIfExists(defaultUrlPath); } 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; }
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"; }
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(); } }
|
初始化,注册命令绑定按键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| 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() {
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); return 1; }) ) );
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); return 1; }) ) );
dispatcher.register( ClientCommandManager.literal("urllist") .executes(context -> { List<String> urls = UrlManager.getUrlList(); 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; }) ); 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); 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 = new KeyBinding( "key.webmapview.open_basic_browser", GLFW.GLFW_KEY_H, "category.webmapview" );
KeyBindingHelper.registerKeyBinding(keyBinding);
final MinecraftClient minecraft = MinecraftClient.getInstance(); 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里面,这样下次打开游戏可以直接用,如果只是写在内存里,关了游戏,数据自然就消失了
1 2 3 4 5
| private static final String URL_FILE_NAME = "urls.txt"; private static final String DEFAULT_URL_FILE_NAME = "default_url.txt"; private static List<String> urlList = new ArrayList<>(); public static String defaultUrl="squaremap-demo.jpenilla.xyz"; public static boolean webmapview=true;
|
工程规范
稍微了解了一下gradle,ai还提了一下maven,默认src是资源,规定i18n要写在lang文件夹下等等。学了这些,我想我不只能看懂自己的模组工程,也能看懂别人的
java
是的,亲手敲代码确实能锻炼java功底,说不定毕业我就是拥有三年工作经验的jvav工程师了😂