From d34dc97671deb4bfa230af64608cbc181803a20c Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Fri, 15 May 2026 21:58:26 +0900 Subject: [PATCH] v0.4.6: server config for auto-preload on join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new: config/video_player.json on first server start. Field preload_urls is a list of HTTP(S) URLs (≤256 chars each) that the server broadcasts via PreloadPayload to every player when they finish joining, so common videos are warmed into each client's video_player_cache/ before they ever play. Reuses the same PreloadPayload + VideoCache path as /videopreload, so chat feedback ("[videopreload] 완료") still applies. - config is loaded once at mod init; invalid entries are dropped with a WARN line. Edit + restart server to apply changes. --- README.md | 40 +++++++- gradle.properties | 2 +- .../ejclaw/videoplayer/VideoPlayerConfig.java | 99 +++++++++++++++++++ .../ejclaw/videoplayer/VideoPlayerMod.java | 18 ++++ 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java diff --git a/README.md b/README.md index 1a620a6..00c60b4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 마인크래프트 안에서 임의의 동영상 URL을 벽·바닥·천장에 평면으로 재생하는 Fabric 모드. - 모드 ID: `video_player` -- 현재 버전: **0.4.5** +- 현재 버전: **0.4.6** - 마인크래프트 버전: **26.1.2** - 필요 Java: **25** (마인크래프트 26.x 가 요구함) @@ -53,7 +53,7 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니 - 받은 `fabric-api-0.149.0+26.1.2.jar` 를 `mods` 폴더에 넣습니다. 2. **video_player** (이 모드) - 다운로드: https://git.tkrmagid.kr/tkrmagid/mc_video_player_mod/releases - - `video_player-0.4.5.jar` 를 다운로드해서 같은 `mods` 폴더에 넣습니다. + - `video_player-0.4.6.jar` 를 다운로드해서 같은 `mods` 폴더에 넣습니다. 이전 버전(`video_player-0.4.0.jar`, `0.4.2.jar`, `0.4.3.jar`, `0.3.x.jar` 등)이 mods 폴더에 남아있다면 **반드시 삭제**하세요. 두 개가 같이 있으면 마인크래프트가 충돌로 켜지지 않습니다. @@ -135,7 +135,7 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니 게임 안에서 채팅창에 `/videostick` 을 입력하세요. 정상이라면: - 인벤토리에 **비디오 스틱** 아이템이 들어옵니다 (보라/검정 missing-texture 가 아니라 작대기 모양 아이콘). -- 보라/검정 missing texture 가 나오면 **STEP 4** 에서 이전 버전 jar(`video_player-0.4.0.jar` / `0.4.1.jar` 등)가 mods 폴더에 같이 남아있는 경우입니다. 다 지우고 `0.4.5` 만 남기고 다시 시작하세요. (0.4.1 이하는 Fabric 26.1.2 model 로더가 unprefixed `item/generated` parent 를 거부해서 스틱 아이콘이 missing-model 큐브로 보입니다 — 0.4.2 에서 수정됨.) +- 보라/검정 missing texture 가 나오면 **STEP 4** 에서 이전 버전 jar(`video_player-0.4.0.jar` / `0.4.1.jar` 등)가 mods 폴더에 같이 남아있는 경우입니다. 다 지우고 `0.4.6` 만 남기고 다시 시작하세요. (0.4.1 이하는 Fabric 26.1.2 model 로더가 unprefixed `item/generated` parent 를 거부해서 스틱 아이콘이 missing-model 큐브로 보입니다 — 0.4.2 에서 수정됨.) --- @@ -193,6 +193,38 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니 > 0.4.5 부터 오디오 거리 감쇠가 **판때기 중앙**을 기준으로 계산됩니다. 예전엔 앵커 블록(보통 화면 모서리)을 기준으로 측정해서 큰 화면일수록 소리가 한쪽에서 들리는 느낌이었습니다. +### 서버 config 로 자동 프리로드 (0.4.6+) + +서버에 모드를 넣고 한 번 실행하면 `<서버폴더>/config/video_player.json` 가 자동 생성됩니다. 이 파일에 자주 쓰는 영상 URL 을 적어두면, 플레이어가 접속할 때마다 서버가 자동으로 그 URL 들의 프리로드 요청을 보냅니다 (= `/videopreload` 를 사람마다 친 것과 같음). + +기본 생성된 파일 예시: + +```json +{ + "_comment": "preload_urls: HTTP(S) video URLs broadcast to every player on join. ...", + "preload_urls": [] +} +``` + +사용 예시 — 인트로 영상과 BGM 영상을 모든 접속자가 미리 받도록: + +```json +{ + "preload_urls": [ + "https://video.example.com/intro.mp4", + "https://video.example.com/bgm-loop.webm" + ] +} +``` + +규칙: + +- 각 URL 은 `http://` 또는 `https://` 시작, 256자 이하 (그 외는 무시되고 서버 로그에 WARN) +- 파일 수정 후 적용하려면 **서버 재시작** 이 필요합니다 (config 는 시작 시 1회만 로딩) +- 접속한 플레이어 화면에는 `/videopreload` 와 동일한 `[videopreload] 다운로드 시작 / 완료` 채팅 메시지가 보입니다 +- 이미 캐시된 URL 은 다시 다운로드하지 않습니다 (SHA-256 캐시 키) +- 싱글플레이는 자체적으로 통합 서버를 띄우므로, 통합 서버의 config 폴더(`<게임폴더>/config/video_player.json`)에도 같은 효과로 동작합니다 + --- ## 알려진 이슈 @@ -208,7 +240,7 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니 JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 ./gradlew build ``` -산출물: `build/libs/video_player-0.4.5.jar` +산출물: `build/libs/video_player-0.4.6.jar` JavaCV를 직접 의존성으로 가져오는 경우의 Maven 좌표: ``` diff --git a/gradle.properties b/gradle.properties index f1d3081..25edc8a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.configuration-cache=false # Mod mod_id=video_player -mod_version=0.4.5 +mod_version=0.4.6 maven_group=com.ejclaw.videoplayer archives_base_name=video_player diff --git a/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java b/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java new file mode 100644 index 0000000..3796897 --- /dev/null +++ b/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java @@ -0,0 +1,99 @@ +package com.ejclaw.videoplayer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Server-side mod config, stored at {@code /config/video_player.json}. + * + *

Format (auto-generated on first start): + *

{@code
+ * {
+ *   // List of HTTP(S) video URLs that the server will tell every player to preload
+ *   // into their local video_player_cache/ folder when they join. Identical to running
+ *   // /videopreload  for each joining player.
+ *   "preload_urls": [
+ *     "https://example.com/intro.mp4"
+ *   ]
+ * }
+ * }
+ * + *

Why a list and not a dedicated tool: the same {@link + * com.ejclaw.videoplayer.net.PreloadPayload} that powers {@code /videopreload} is reused, so the + * client-side cache, chat feedback, and {@code /videoplace} → cache lookup paths all behave + * identically for config-driven and command-driven preloads. + */ +public final class VideoPlayerConfig { + private VideoPlayerConfig() {} + + private static final String FILE_NAME = "video_player.json"; + private static volatile List PRELOAD_URLS = Collections.emptyList(); + + /** Load (or create) the config file. Called once during mod initialization. */ + public static void load() { + Path path = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME); + try { + if (!Files.exists(path)) { + writeDefault(path); + VideoPlayerMod.LOG.info("[{}] created default config at {}", + VideoPlayerMod.MOD_ID, path); + PRELOAD_URLS = Collections.emptyList(); + return; + } + String raw = Files.readString(path, StandardCharsets.UTF_8); + JsonObject json = JsonParser.parseString(raw).getAsJsonObject(); + List urls = new ArrayList<>(); + if (json.has("preload_urls") && json.get("preload_urls").isJsonArray()) { + json.getAsJsonArray("preload_urls").forEach(el -> { + if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()) { + String u = el.getAsString().trim(); + if (!u.isEmpty() && (u.startsWith("http://") || u.startsWith("https://")) + && u.length() <= 256) { + urls.add(u); + } else if (!u.isEmpty()) { + VideoPlayerMod.LOG.warn( + "[{}] config: ignoring invalid preload url '{}' (must be http/https, ≤256 chars)", + VideoPlayerMod.MOD_ID, u); + } + } + }); + } + PRELOAD_URLS = Collections.unmodifiableList(urls); + VideoPlayerMod.LOG.info("[{}] config loaded: {} preload url(s)", + VideoPlayerMod.MOD_ID, urls.size()); + } catch (Throwable t) { + VideoPlayerMod.LOG.warn("[{}] failed to read config {}: {} — using empty list", + VideoPlayerMod.MOD_ID, path, t.toString()); + PRELOAD_URLS = Collections.emptyList(); + } + } + + /** URLs to push to each joining player. Never null; possibly empty. */ + public static List preloadUrls() { + return PRELOAD_URLS; + } + + private static void writeDefault(Path path) throws IOException { + Files.createDirectories(path.getParent()); + // Hand-rolled rather than Gson-serialized so we can carry a `_comment` field that + // explains the format directly inside the file. + JsonObject root = new JsonObject(); + root.addProperty("_comment", + "preload_urls: HTTP(S) video URLs broadcast to every player on join. " + + "Equivalent to running /videopreload per joiner. Max 256 chars per url."); + root.add("preload_urls", new com.google.gson.JsonArray()); + Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + Files.writeString(path, gson.toJson(root), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/ejclaw/videoplayer/VideoPlayerMod.java b/src/main/java/com/ejclaw/videoplayer/VideoPlayerMod.java index db6698e..009e007 100644 --- a/src/main/java/com/ejclaw/videoplayer/VideoPlayerMod.java +++ b/src/main/java/com/ejclaw/videoplayer/VideoPlayerMod.java @@ -5,12 +5,15 @@ import com.ejclaw.videoplayer.command.VideoMuteCommand; import com.ejclaw.videoplayer.command.VideoPlaceCommand; import com.ejclaw.videoplayer.command.VideoPreloadCommand; import com.ejclaw.videoplayer.command.VideoStickCommand; +import com.ejclaw.videoplayer.net.PreloadPayload; import com.ejclaw.videoplayer.net.VideoPlayerNetwork; import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities; import com.ejclaw.videoplayer.registry.VideoPlayerBlocks; import com.ejclaw.videoplayer.registry.VideoPlayerItems; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +30,8 @@ public class VideoPlayerMod implements ModInitializer { VideoPlayerNetwork.registerPayloadTypes(); VideoPlayerNetwork.registerServerReceivers(); + VideoPlayerConfig.load(); + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, env) -> { VideoStickCommand.register(dispatcher); VideoPlaceCommand.register(dispatcher); @@ -35,6 +40,19 @@ public class VideoPlayerMod implements ModInitializer { VideoPreloadCommand.register(dispatcher); }); + // When a player finishes joining, push every preload URL from the config so their + // client kicks off the background download. Reuses the same PreloadPayload that + // /videopreload sends, so the client-side caching path is identical. + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + java.util.List urls = VideoPlayerConfig.preloadUrls(); + if (urls.isEmpty()) return; + for (String url : urls) { + ServerPlayNetworking.send(handler.getPlayer(), new PreloadPayload(url)); + } + LOG.info("[{}] sent {} config preload(s) to {}", + MOD_ID, urls.size(), handler.getPlayer().getName().getString()); + }); + LOG.info("[{}] initialized", MOD_ID); } }