diff --git a/.gitignore b/.gitignore index cb2b3ee..b4a980a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,53 @@ -# Gradle 构建产物 +# Gradle / 构建产物 .gradle/ build/ out/ +# Minecraft 运行时 +run/ +runs/ +logs/ + +# 已废弃的流式 Demo(仅剩运行时残壳,不纳入仓库) +streamdemo/ + # IDE .idea/ .vscode/ *.iml +bin/ -# 运行时数据 -runs/ -logs/ +# 参考/分析目录(MCMT、Tessellate、folia、原版源码等,体量大不进仓库) +_mcmt_analysis/ +_mcmt* +_chlorophyll_analysis/ +_dimthreads_ref/ +_tessellate_analysis/ +_src_tmp/ +_srcpkg/ +_tmp_inspect/ +_tmpclass/ +folia-1.21.1-src/ +vanilla-1.21.1-src/ +craftengine-demo/ + +# 调试诊断残留 *.log +*_dump*.txt +threaddump*.txt +thread_dump*.txt +jstack*.txt +deadlock*.txt +runcheck*.txt +runclient_log.txt +runclient_launch.log +game_deadlock.txt +worldcreate_stuck.txt +capture_walk.ps1 -# 系统文件 +# 历史打包产物 +modzuozhi-*.jar + +# 系统 .DS_Store -Thumbs.db +Thumbs.db \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7402740..02d2053 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,14 @@ java { withSourcesJar() } +// 方案A验证用:无 mod 源码集(仅承载 Demo 资源,不注册任何模组) +sourceSets { + demo { + java { srcDir 'src/demo/java' } + resources { srcDir 'src/demo/resources' } + } +} + //minecraft.accessTransformers.file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg') //minecraft.accessTransformers.publish rootProject.file('src/main/resources/META-INF/accesstransformer.cfg') @@ -32,21 +40,23 @@ runs { //systemProperty 'forge.logging.console.level', 'debug' systemProperty 'mixin.debug.verbose', 'true' systemProperty 'mixin.debug.export', 'true' - modSource project.sourceSets.main } client { systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id // TEMP-DEBUG: auto-join world for deadlock repro (REMOVE AFTER DEBUG) - argument '--quickPlaySingleplayer' - argument '新的世界' + // 临时注释:方案A验证需手动连 Demo 服务器(不自动进单人) + //argument '--quickPlaySingleplayer' + //argument '新的世界' // 关闭 JVM 热路径堆栈省略,便于定位并行子任务中反复抛出的 ClassCastException jvmArguments '-XX:-OmitStackTraceInFastThrow' + modSource project.sourceSets.main } server { systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id argument '--nogui' + modSource project.sourceSets.main } gameTestServer { diff --git a/gradle.properties b/gradle.properties index 65d7b55..98a7390 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,6 @@ # Sets default memory used for gradle commands. Can be overridden by user or command line properties. # This is required to provide enough memory for the Minecraft decompilation process. -# 本机 JDK 路径(机器特定,已从仓库中注释掉)。如本地构建报"无法找到 JDK",请取消注释并填你本机的 JDK 路径。 -#org.gradle.java.home=C:\\Program Files\\Java\\jdk-21 +org.gradle.java.home=C:\\Program Files\\Java\\jdk-21 org.gradle.jvmargs=-Xmx3G org.gradle.daemon=true org.gradle.parallel=true @@ -38,7 +37,7 @@ mod_name=Dimensional Ripper # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=MIT # The mod version. See https://semver.org/ -mod_version=1.0.0 +mod_version=1.1.1 # The group ID for the mod. It is only important when publishing build artifacts to Maven. mod_group_id=dev.modzuozhi # The authors of the mod. This is a simple text string that is used for display purposes in the mod list. diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..6ac5c68 --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,56 @@ +# NeoForge 1.21.1 开发环境初始化脚本 +# 运行一次即可:下载 Gradle Wrapper JAR 并同步依赖 + +$ErrorActionPreference = "Stop" +$projectDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$wrapperJar = Join-Path $projectDir "gradle\wrapper\gradle-wrapper.jar" +$wrapperUrl = "https://raw.githubusercontent.com/gradle/gradle/v8.8.0/gradle/wrapper/gradle-wrapper.jar" + +# 如果 wrapper jar 不存在则下载 +if (-not (Test-Path $wrapperJar)) { + Write-Host "Downloading gradle-wrapper.jar ..." -ForegroundColor Cyan + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $wrapperJar) | Out-Null + try { + Invoke-WebRequest -Uri $wrapperUrl -OutFile $wrapperJar -UseBasicParsing + } catch { + Write-Host "Failed to download wrapper jar from GitHub, trying local Gradle installations ..." -ForegroundColor Yellow + $found = $false + $localPaths = @( + "C:\Program Files\Gradle\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar", + "C:\ProgramData\chocolatey\lib\gradle\tools\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar", + "$env:USERPROFILE\.gradle\wrapper\dists\gradle-8.8-bin\*\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar", + "$env:USERPROFILE\scoop\apps\gradle\8.8\lib\plugins\gradle-wrapper-8.8.jar" + ) + foreach ($p in $localPaths) { + $matches = Get-Item -Path $p -ErrorAction SilentlyContinue + if ($matches) { + Copy-Item -Path $matches[0].FullName -Destination $wrapperJar -Force + Write-Host "Copied gradle-wrapper.jar from $($matches[0].FullName)" -ForegroundColor Green + $found = $true + break + } + } + if (-not $found) { + Write-Host "Local copy not found, trying services.gradle.org ..." -ForegroundColor Yellow + $wrapperUrl = "https://services.gradle.org/distributions/gradle-8.8-bin.zip" + $zip = Join-Path $env:TEMP "gradle-8.8-bin.zip" + Invoke-WebRequest -Uri $wrapperUrl -OutFile $zip -UseBasicParsing + Expand-Archive -Path $zip -DestinationPath (Join-Path $env:TEMP "gradle-8.8") -Force + $srcJar = Join-Path $env:TEMP "gradle-8.8\gradle-8.8\lib\plugins\gradle-wrapper-8.8.jar" + if (Test-Path $srcJar) { + Copy-Item -Path $srcJar -Destination $wrapperJar -Force + } else { + throw "Could not locate gradle-wrapper.jar" + } + } + } + Write-Host "gradle-wrapper.jar ready." -ForegroundColor Green +} else { + Write-Host "gradle-wrapper.jar already exists." -ForegroundColor Green +} + +# 首次 Gradle 同步 +Write-Host "Running first Gradle sync (this may take a while) ..." -ForegroundColor Cyan +& (Join-Path $projectDir "gradlew.bat") --refresh-dependencies + +Write-Host "Setup complete. You can now run 'gradlew runClient' or 'gradlew build'." -ForegroundColor Green diff --git a/src/main/java/dev/modzuozhi/command/MasterCommand.java b/src/main/java/dev/modzuozhi/command/MasterCommand.java index b3fe7c3..49b1db3 100644 --- a/src/main/java/dev/modzuozhi/command/MasterCommand.java +++ b/src/main/java/dev/modzuozhi/command/MasterCommand.java @@ -4,6 +4,7 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.context.CommandContext; import dev.modzuozhi.ModZuozhi; import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.EntityTickParallel; import dev.modzuozhi.core.dimthread.FaultGuard; import dev.modzuozhi.core.dimthread.FineGrainScheduler; import dev.modzuozhi.core.dimthread.TpsTracker; @@ -36,7 +37,10 @@ public final class MasterCommand { .then(literal("status").executes(MasterCommand::status)) .then(literal("fine") .then(literal("on").executes(ctx -> setFine(ctx, true))) - .then(literal("off").executes(ctx -> setFine(ctx, false)))) + .then(literal("off").executes(ctx -> setFine(ctx, false))) + .then(literal("sharded") + .then(literal("on").executes(ctx -> setSharded(ctx, true))) + .then(literal("off").executes(ctx -> setSharded(ctx, false))))) .then(literal("nocollide") .then(literal("on").executes(ctx -> setNoCollide(ctx, true))) .then(literal("off").executes(ctx -> setNoCollide(ctx, false))))); @@ -67,6 +71,13 @@ public final class MasterCommand { return 1; } + private static int setSharded(CommandContext ctx, boolean enabled) { + EntityTickParallel.setSharded(enabled); + ctx.getSource().sendSuccess(() -> + Component.literal("实体分片并行已" + (enabled ? "开启" : "关闭")), false); + return 1; + } + private static int setNoCollide(CommandContext ctx, boolean enabled) { ModZuozhi.MOB_COLLISION_OFF = enabled; ctx.getSource().sendSuccess(() -> @@ -92,9 +103,12 @@ public final class MasterCommand { : TpsTracker.tps(); String msg = String.format( - "modzuozhi 状态:细粒度=%s%s,禁碰撞=%s,故障原因:%s\n当前维度:%s,TPS:%.1f", - fine, fineMark, noCollide, reason, - dimensionName(dimension), tps); + "modzuozhi 状态:细粒度=%s%s,分片并行=%s,禁碰撞=%s,故障原因:%s\n" + + "当前维度:%s,TPS:%.1f,掉拍分片累计:%d", + fine, fineMark, + EntityTickParallel.isSharded() ? "开启" : "关闭", + noCollide, reason, + dimensionName(dimension), tps, EntityTickParallel.missedShards()); ctx.getSource().sendSuccess(() -> Component.literal(msg), false); return 1; } diff --git a/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java b/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java index 9cf868a..dfb92ac 100644 --- a/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java +++ b/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java @@ -1,12 +1,17 @@ package dev.modzuozhi.core.dimthread; +import dev.modzuozhi.ModZuozhi; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Consumer; @@ -37,6 +42,21 @@ public final class EntityTickParallel { /** 分片读写锁数组:保护单个 {@code EntitySection} 的内容读写({@code ClassInstanceMultiMap})。 */ private static final ReentrantReadWriteLock[] SECTION_LOCKS = new ReentrantReadWriteLock[STRIPE_COUNT]; + /** + * 实体分片 Claim 门表:按维度隔离(不同维度的实体循环各自独立 claim,互不掉拍)。 + *

+ * 门状态跨 tick 存续:本 tick 提交的某片未完成前,下 tick 对该片 tryAcquire 失败 → + * 整片掉拍延后(等价"该片本 tick 视为空")。阶段收口 {@link FineGrainScheduler.Barrier#await()} + * 只等待"实际提交的片",慢片不再阻塞维度 worker(借鉴 Tessellate 慢区域掉拍思想)。 + */ + private static final ConcurrentHashMap, ShardGate> GATES = new ConcurrentHashMap<>(); + + /** 累计掉拍分片数(诊断 / status 展示)。 */ + private static final LongAdder MISSED_SHARDS = new LongAdder(); + + /** 实体分片并行的独立开关(默认开启;关闭时回退旧的批式 submitBatch 路径)。 */ + private static volatile boolean sharded = true; + /** * 当前线程的锁配对栈:记录每次 lock 调用时是否真正加了锁,unlock 时弹出对应决定。 *

@@ -137,19 +157,64 @@ public final class EntityTickParallel { return CURRENT.get(); } + /** 实体分片并行开关(默认开启;关闭时回退旧的批式 {@code submitBatch} 路径)。 */ + public static boolean isSharded() { + return sharded; + } + + public static void setSharded(boolean value) { + sharded = value; + ModZuozhi.LOGGER.info("[EntityTick] 实体分片并行已切换为 {}", value ? "开启" : "关闭"); + } + + /** 累计掉拍分片数(诊断 / {@code status} 展示)。 */ + public static long missedShards() { + return MISSED_SHARDS.sum(); + } + /** * 本 tick 收集的一批待并行实体(同一 consumer,即 {@code tickNonPassenger})。 - * 收集代替逐实体提交:循环结束后统一按线程池大小切成块提交,把每实体一次的 - * 调度/屏障计数开销从 O(实体数) 降到 O(线程数)。 + * 收集代替逐实体提交:循环结束后按稳定 id 分片,每片独立提交(分片 Claim 模型), + * 或按线程池大小切成块提交(非分片回退),把每实体一次的调度/屏障计数开销降到 O(线程数)。 */ public static final class Batch { public final MinecraftServer server; public final Consumer consumer; - public final List entities = new ArrayList<>(); + public final ResourceKey dimension; + /** 按分片存放的实体列表(索引 = entity.getId() & (SHARDS-1))。 */ + private final List[] shards = new List[ShardGate.SHARDS]; - public Batch(MinecraftServer server, Consumer consumer) { + public Batch(MinecraftServer server, Consumer consumer, ResourceKey dimension) { this.server = server; this.consumer = consumer; + this.dimension = dimension; + } + + /** 收集一个实体:按稳定 id 分流到对应分片(id 自增、分布均匀)。 */ + public void add(Entity entity) { + int shard = entity.getId() & (ShardGate.SHARDS - 1); + List list = this.shards[shard]; + if (list == null) { + list = new ArrayList<>(); + this.shards[shard] = list; + } + list.add(entity); + } + + /** 全部分片数组(end 遍历用)。 */ + public List[] allShards() { + return this.shards; + } + + /** 汇总全量实体列表(仅非分片回退路径使用)。 */ + public List merged() { + List all = new ArrayList<>(); + for (List shard : this.shards) { + if (shard != null) { + all.addAll(shard); + } + } + return all; } } @@ -157,10 +222,10 @@ public final class EntityTickParallel { public static void collect(MinecraftServer server, Consumer consumer, Entity entity) { Batch batch = BATCH.get(); if (batch == null) { - batch = new Batch(server, consumer); + batch = new Batch(server, consumer, entity.level().dimension()); BATCH.set(batch); } - batch.entities.add(entity); + batch.add(entity); } /** 实体循环开始:为本维度 tick 建立实体子任务屏障(仅 worker 线程调用)。 */ @@ -169,8 +234,8 @@ public final class EntityTickParallel { } /** - * 实体循环结束(finally 中调用):先把收集的实体批按块提交并行,再等待全部子任务完成 - * 并执行延迟回调,清除上下文。 + * 实体循环结束(finally 中调用):分片模式先按片 Claim 提交(慢片掉拍延后), + * 再等待已提交的子任务全部完成并执行延迟回调,清除上下文。 */ public static void end() { FineGrainScheduler.Barrier barrier = CURRENT.get(); @@ -180,10 +245,45 @@ public final class EntityTickParallel { CURRENT.remove(); Batch batch = BATCH.get(); BATCH.remove(); - if (batch != null && !batch.entities.isEmpty()) { - barrier.submitBatch(batch.server, batch.entities, batch.consumer); + if (batch != null) { + if (sharded) { + ShardGate gate = GATES.computeIfAbsent(batch.dimension, key -> new ShardGate()); + List[] shards = batch.allShards(); + for (int s = 0; s < ShardGate.SHARDS; s++) { + List entities = shards[s]; + if (entities == null || entities.isEmpty()) { + continue; + } + if (gate.tryAcquire(s)) { + barrier.submitShard(batch.server, gate, s, + () -> modzuozhi_tickShard(entities, batch.consumer)); + } else { + MISSED_SHARDS.increment(); + ModZuozhi.LOGGER.debug("[EntityTick] 实体分片 {} 掉拍(上 tick 未完成),整体延后 1 tick", s); + } + } + } else { + List all = batch.merged(); + if (!all.isEmpty()) { + barrier.submitBatch(batch.server, all, batch.consumer); + } + } } barrier.await(); barrier.drain(); } + + /** 分片内逐实体 tick:单实体异常隔离(与批式路径语义一致),异常计入 FaultGuard。 */ + private static void modzuozhi_tickShard(List entities, Consumer consumer) { + for (Entity entity : entities) { + try { + if (!entity.isRemoved()) { + consumer.accept(entity); + } + } catch (Throwable t) { + ModZuozhi.LOGGER.error("[EntityTick] 实体分片子任务异常", t); + FaultGuard.onSubTaskFailure(); + } + } + } } diff --git a/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java b/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java index b241a4d..f378ad1 100644 --- a/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java +++ b/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java @@ -247,6 +247,52 @@ public final class FineGrainScheduler { } } + /** + * 按分片提交一个子任务(分片 Claim 模型,借鉴 Tessellate 的"每区域 inFlight CAS claim")。 + *

+ * 调用方必须先通过 {@link ShardGate#tryAcquire} 取得本片许可;本方法在任务完成后 + * {@link ShardGate#release} 释放 claim。已提交该片的 {@code pending} 计入 barrier, + * 阶段末 {@link #await()} 只等待"实际提交的片"——掉拍(未抢到 claim)的片不在 + * pending 内,维度 worker 不会为其阻塞。 + *

+ * 关闭({@code enabled=false})时同步串行执行并释放 claim,行为与原串行路径一致。 + * + * @param gate 分片门(跨 tick 存续) + * @param shard 分片索引 + * @param task 子任务(如某一片实体的 tick) + */ + public void submitShard(MinecraftServer server, ShardGate gate, int shard, Runnable task) { + if (!FineGrainScheduler.enabled) { + gate.release(shard); + task.run(); + return; + } + synchronized (this) { + pending++; + } + ACTIVE_SUBTASKS.incrementAndGet(); + SUB_POOL.execute(() -> { + PostExecuteQueue prev = CURRENT_QUEUE.get(); + CURRENT_QUEUE.set(postQueue); + try { + task.run(); + } catch (Throwable t) { + ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t); + FaultGuard.onSubTaskFailure(); + } finally { + CURRENT_QUEUE.set(prev); + gate.release(shard); + synchronized (this) { + pending--; + if (pending == 0) { + latch.countDown(); + } + } + ACTIVE_SUBTASKS.decrementAndGet(); + } + }); + } + /** 块内逐实体 tick:单实体异常隔离(等价原逐实体提交语义),异常计入 FaultGuard。 */ private void modzuozhi_runChunk(List chunk, Consumer consumer) { PostExecuteQueue prev = CURRENT_QUEUE.get(); diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ShardGate.java b/src/main/java/dev/modzuozhi/core/dimthread/ShardGate.java new file mode 100644 index 0000000..d5e8b94 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/ShardGate.java @@ -0,0 +1,46 @@ +package dev.modzuozhi.core.dimthread; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 分片 Claim 门(借鉴 Tessellate 的"每区域一个 inFlight CAS claim、慢区掉拍"思想独立实现)。 + *

+ * 一片在同一时刻至多允许一个任务运行,claim 状态跨 tick 存续:本 tick 提交的任务在 + * {@link #tryAcquire} 之后、任务完成 {@link #release} 之前,下 tick 对该片的抢占必然失败 → + * 整片掉拍延后(等价"该片本 tick 视为空",与 MC 实体密度限制同性质,无感知)。 + *

+ * 收益:阶段收口从"等全部子任务"(慢片拖累全体)变成"只等已提交且上一 tick 已完成过的片", + * 慢片不再阻塞维度 worker。 + *

+ * 线程安全:仅使用 CAS 置位,无锁、无等待。 + */ +public final class ShardGate { + + /** + * 分片数:2 的幂(供 & 掩码),最低 8,按 CPU 核数线性扩展(≈ 2×核数)。 + */ + public static final int SHARDS = Integer.highestOneBit(Math.max(8, Runtime.getRuntime().availableProcessors() * 2)); + + /** 每片一个 claim 位(false=空闲,true=本 tick 有任务在跑/已提交)。 */ + private final AtomicBoolean[] gates = new AtomicBoolean[SHARDS]; + + public ShardGate() { + for (int i = 0; i < SHARDS; i++) { + gates[i] = new AtomicBoolean(false); + } + } + + /** + * CAS false→true 抢占本片。 + * + * @return true=允许提交(本片空闲,已占用);false=上一 tick 该片任务尚未完成(掉拍)。 + */ + public boolean tryAcquire(int shard) { + return gates[shard].compareAndSet(false, true); + } + + /** 本片任务完成时释放 claim,允许下 tick 重新提交。 */ + public void release(int shard) { + gates[shard].set(false); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java b/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java index eee6ad6..c17e3cb 100644 --- a/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java +++ b/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java @@ -1,8 +1,7 @@ package dev.modzuozhi.mixin; +import dev.modzuozhi.core.collection.ConcurrentLong2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minecraft.world.level.chunk.storage.SectionStorage; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -19,13 +18,19 @@ import java.util.Optional; * {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}), * 进而引发 chunk 加载失败、连锁异常。 *

- * 修复:把 {@code storage} 替换为 {@link Long2ObjectMaps#synchronize 同步包装},所有 - * get/put/remove/values 访问线程安全,从根上消除该数据竞争。 + * 旧修复:把 {@code storage} 替换为 {@code Long2ObjectMaps.synchronize} 同步包装,数据安全了, + * 但全表互斥把村民 POI 查询(读多写少、原本可并行)串行化:19000 只村民压测下 + * 19 个子任务线程 16 个 BLOCKED 在同一把锁上,成为并行 tick 的串行瓶颈(TPS 9 / CPU 半闲)。 + *

+ * 新修复:换成 {@link ConcurrentLong2ObjectMap}({@code ConcurrentHashMap} 底座, + * {@code SectionStorage} 对 storage 的全部调用 get/put/remove/size 均已覆盖)—— + * 读路径无锁并行,写路径(区块加载 put、方块变化)分段安全,弱一致迭代兼容 + * {@code writeColumn} 的读取。POI 查询恢复并行,消除该锁地板。 */ @Mixin(SectionStorage.class) public abstract class SectionStorageMixin { @Shadow private final Long2ObjectMap> storage = - Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); + new ConcurrentLong2ObjectMap<>(); } diff --git a/初始化环境.bat b/初始化环境.bat new file mode 100644 index 0000000..7cf23d9 --- /dev/null +++ b/初始化环境.bat @@ -0,0 +1,25 @@ +@echo off +chcp 65001 >nul +REM 一键初始化 NeoForge 1.21.1 开发环境 +REM 会自动下载 gradle-wrapper.jar 并刷新依赖 + +cd /d "%~dp0" + +echo ======================================== +echo 正在初始化 NeoForge 1.21.1 开发环境 +echo ======================================== +echo. + +powershell -ExecutionPolicy Bypass -File "setup.ps1" + +if %errorlevel% neq 0 ( + echo. + echo 初始化过程中出现错误,请检查网络连接或手动下载 gradle-wrapper.jar。 + pause + exit /b %errorlevel% +) + +echo. +echo 初始化完成! +echo 你现在可以运行:gradlew runClient +cmd /k diff --git a/启动环境.bat b/启动环境.bat new file mode 100644 index 0000000..48cf383 --- /dev/null +++ b/启动环境.bat @@ -0,0 +1,24 @@ +@echo off +REM NeoForge 1.21.1 模组开发环境启动脚本 +set JAVA_HOME=C:\Program Files\Java\jdk-21 +set PATH=%JAVA_HOME%\bin;%PATH% + +echo ======================================== +echo Minecraft 1.21.1 NeoForge 开发环境 +echo ======================================== +echo. +echo Java版本: +java -version +echo. +echo Gradle任务列表: +echo runClient - 启动Minecraft客户端测试 +echo runServer - 启动专用服务器测试 +echo runData - 运行数据生成器 +echo build - 构建模组JAR文件 +echo idea - 生成IntelliJ IDEA项目文件 +echo. +echo 当前目录: %cd% +echo. +echo 输入 gradlew 任务名 来执行,例如: gradlew runClient +echo. +cmd /k