From b09299975e838591bd0ec86a6a7969572c8ca4ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:33:46 +0000 Subject: [PATCH 1/2] Initial plan From 89cb0491b0322f33d1489ad67ecbe5655e31c8b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:35:29 +0000 Subject: [PATCH 2/2] chore: remove all em dashes, replace with hyphen-minus Agent-Logs-Url: https://github.com/BENZOOgataga/FrameShift/sessions/cf4b0fc1-7d5d-415c-acdd-6630a5c9e73c Co-authored-by: BENZOOgataga <50145143+BENZOOgataga@users.noreply.github.com> --- .gitignore | 6 ++-- CLAUDE.md | 32 +++++++++---------- README.md | 12 +++---- .../2026-04-24-schematic-parsing-list-info.md | 32 +++++++++---------- ...4-24-schematic-parsing-list-info-design.md | 24 +++++++------- 5 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index c6b803b..fdb9e49 100644 --- a/.gitignore +++ b/.gitignore @@ -15,21 +15,21 @@ src/generated/resources/ # Logs *.log -# IDE — IntelliJ +# IDE - IntelliJ .idea/ *.iml *.ipr *.iws out/ -# IDE — Eclipse +# IDE - Eclipse .eclipse/ .classpath .project .settings/ bin/ -# IDE — VS Code +# IDE - VS Code .vscode/ # OS diff --git a/CLAUDE.md b/CLAUDE.md index 1622fd1..4e9631a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -FrameShift is a NeoForge 1.21.1 server-side mod for loading and pasting schematics without causing server lag or crashes. Performance safety over speed — the system must degrade gracefully rather than freeze or crash the server. +FrameShift is a NeoForge 1.21.1 server-side mod for loading and pasting schematics without causing server lag or crashes. Performance safety over speed - the system must degrade gracefully rather than freeze or crash the server. -Author: BENZOOgataga (benzoogataga.com) — Mod ID: `frameshift` — Group: `com.benzoogataga.frameshift` +Author: BENZOOgataga (benzoogataga.com) - Mod ID: `frameshift` - Group: `com.benzoogataga.frameshift` ## Working With This User -The project owner is a Java/modding beginner — do not assume familiarity with NeoForge APIs, Gradle, or Minecraft internals. When explaining or implementing anything: +The project owner is a Java/modding beginner - do not assume familiarity with NeoForge APIs, Gradle, or Minecraft internals. When explaining or implementing anything: - Prefer clear, readable code over clever abstractions - Add a one-line comment to every non-obvious method or field - Explain NeoForge concepts (event buses, mod containers, config specs, etc.) in plain language when they come up @@ -28,7 +28,7 @@ On Windows, `./gradlew` works in Git Bash. Use `gradlew.bat` in CMD/PowerShell. ## Toolchain (Non-Negotiable Versions) -These versions were arrived at after resolving real compatibility conflicts — do not upgrade without careful testing: +These versions were arrived at after resolving real compatibility conflicts - do not upgrade without careful testing: | Tool | Version | Reason | |------|---------|--------| @@ -39,33 +39,33 @@ These versions were arrived at after resolving real compatibility conflicts — ## Known Build Quirks -**ASM version conflict** — NeoForge's bootstraplauncher crashes if different ASM versions appear on the module path vs classpath. `build.gradle` forces all `org.ow2.asm:*` to `9.8` via `resolutionStrategy`. Do not remove this block. +**ASM version conflict** - NeoForge's bootstraplauncher crashes if different ASM versions appear on the module path vs classpath. `build.gradle` forces all `org.ow2.asm:*` to `9.8` via `resolutionStrategy`. Do not remove this block. -**Mod descriptor filename** — NeoForge 1.21+ uses `META-INF/neoforge.mods.toml`. The old `META-INF/mods.toml` name causes a "for Minecraft Forge or older NeoForge" load error. Do not rename it back. +**Mod descriptor filename** - NeoForge 1.21+ uses `META-INF/neoforge.mods.toml`. The old `META-INF/mods.toml` name causes a "for Minecraft Forge or older NeoForge" load error. Do not rename it back. -**processResources** — Uses `def replaceProperties` (not `var`) for Groovy compatibility. The `expand()` call substitutes `${...}` placeholders in `neoforge.mods.toml` at build time. `${file.jarVersion}` is a NeoForge runtime token and must NOT be in that map — use `${mod_version}` instead. +**processResources** - Uses `def replaceProperties` (not `var`) for Groovy compatibility. The `expand()` call substitutes `${...}` placeholders in `neoforge.mods.toml` at build time. `${file.jarVersion}` is a NeoForge runtime token and must NOT be in that map - use `${mod_version}` instead. -**Gradle wrapper args** — The `gradlew` script uses the `set --` + `xargs` pattern to forward CLI arguments to `GradleWrapperMain`. The simplified single-`eval` form does not pass arguments and silently runs the default `help` task instead. +**Gradle wrapper args** - The `gradlew` script uses the `set --` + `xargs` pattern to forward CLI arguments to `GradleWrapperMain`. The simplified single-`eval` form does not pass arguments and silently runs the default `help` task instead. -**NeoForge event subscriptions** — The `@Mod` constructor receives `IEventBus modEventBus` (for mod lifecycle events) and `ModContainer modContainer` (for config registration). Config is registered via `modContainer.registerConfig(ModConfig.Type.SERVER, spec)`, not via `ModLoadingContext`. +**NeoForge event subscriptions** - The `@Mod` constructor receives `IEventBus modEventBus` (for mod lifecycle events) and `ModContainer modContainer` (for config registration). Config is registered via `modContainer.registerConfig(ModConfig.Type.SERVER, spec)`, not via `ModLoadingContext`. -**Runs DSL** — In NeoGradle 7.0.165, use `argument`/`arguments` (not the deprecated `programArgument`/`programArguments`). +**Runs DSL** - In NeoGradle 7.0.165, use `argument`/`arguments` (not the deprecated `programArgument`/`programArguments`). ## Package Structure ``` com.benzoogataga.frameshift/ -├── FrameShift.java @Mod entry point — wires event buses and config +├── FrameShift.java @Mod entry point - wires event buses and config ├── command/ │ └── SchemCommand.java /schem command tree (TODO) ├── schematic/ │ ├── SchematicData.java parsed file holder (dimensions, blocks, BEs, entities) -│ └── SchematicLoader.java async file parser — returns CompletableFuture (TODO) +│ └── SchematicLoader.java async file parser - returns CompletableFuture (TODO) ├── job/ │ ├── SchematicPasteJob.java job model: state enum + 3 ArrayDeque queues │ └── JobManager.java static registry, enforces maxConcurrentJobs ├── tick/ -│ └── TickHandler.java @SubscribeEvent ServerTickEvent.Post — drains queues (TODO) +│ └── TickHandler.java @SubscribeEvent ServerTickEvent.Post - drains queues (TODO) ├── config/ │ └── FrameShiftConfig.java all ModConfigSpec entries, registered SERVER type └── chunk/ @@ -101,8 +101,8 @@ com.benzoogataga.frameshift/ - `maxMillisPerTick = 8` ### Supported Formats -1. `.schem` (Sponge — primary) -2. `.schematic` (legacy — optional) +1. `.schem` (Sponge - primary) +2. `.schematic` (legacy - optional) Both require full block state, block entity (NBT), and optional entity support. @@ -120,7 +120,7 @@ schematics/ |---------|---------| | `/schem list` | List available schematics with dimensions, block count, file size | | `/schem info ` | Show dimensions, volume, block entities, estimated paste time | -| `/schem paste [x y z]` | Paste — flags: `--ignore-air`, `--include-entities`, `--include-block-entities`, `--rotation`, `--mirror` | +| `/schem paste [x y z]` | Paste - flags: `--ignore-air`, `--include-entities`, `--include-block-entities`, `--rotation`, `--mirror` | | `/schem status` | Progress, blocks placed/remaining, ETA | | `/schem cancel/pause/resume ` | Job lifecycle control | | `/schem reload` | Reload config | diff --git a/README.md b/README.md index 0469c6f..a20b5a7 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ A NeoForge 1.21.1 server-side mod for loading and pasting schematics without causing lag spikes or crashes. -**Performance safety over speed** — the system degrades gracefully under load instead of freezing the server. +**Performance safety over speed** - the system degrades gracefully under load instead of freezing the server. --- ## Features -- Progressive block placement — schematics are pasted over many ticks, never all at once -- Adaptive throttling — automatically slows down when the server is struggling (based on MSPT) -- Job control — pause, resume, and cancel pastes mid-way through -- Full NBT support — block entities (chests, signs, spawners) and entities are preserved -- Configurable limits — tune blocks-per-tick, time budget, concurrent jobs, and more +- Progressive block placement - schematics are pasted over many ticks, never all at once +- Adaptive throttling - automatically slows down when the server is struggling (based on MSPT) +- Job control - pause, resume, and cancel pastes mid-way through +- Full NBT support - block entities (chests, signs, spawners) and entities are preserved +- Configurable limits - tune blocks-per-tick, time budget, concurrent jobs, and more ## Supported Formats diff --git a/docs/superpowers/plans/2026-04-24-schematic-parsing-list-info.md b/docs/superpowers/plans/2026-04-24-schematic-parsing-list-info.md index 15d2bae..abeb8e5 100644 --- a/docs/superpowers/plans/2026-04-24-schematic-parsing-list-info.md +++ b/docs/superpowers/plans/2026-04-24-schematic-parsing-list-info.md @@ -61,7 +61,7 @@ test { ./gradlew test --tests "com.nonexistent.*" ``` -Expected: task runs and exits with "No tests found" (or 0 failures) — not a build error. +Expected: task runs and exits with "No tests found" (or 0 failures) - not a build error. --- @@ -104,7 +104,7 @@ class SchematicFormatTest { ./gradlew test --tests "com.benzoogataga.frameshift.schematic.SchematicFormatTest" ``` -Expected: compilation failure — `SchematicFormat` doesn't exist yet. +Expected: compilation failure - `SchematicFormat` doesn't exist yet. - [ ] **Step 3: Create `SchematicFormat.java`** @@ -199,7 +199,7 @@ class SchematicMetadataTest { ./gradlew test --tests "com.benzoogataga.frameshift.schematic.SchematicMetadataTest" ``` -Expected: compilation error — `SchematicMetadata` doesn't exist. +Expected: compilation error - `SchematicMetadata` doesn't exist. - [ ] **Step 3: Create `SchematicMetadata.java`** @@ -412,7 +412,7 @@ package com.benzoogataga.frameshift.schematic; import java.io.IOException; -// Streaming iterator over block entries in a schematic — used by future paste implementation +// Streaming iterator over block entries in a schematic - used by future paste implementation public interface BlockStream extends AutoCloseable { boolean hasNext() throws IOException; SchematicBlockEntry next() throws IOException; @@ -430,13 +430,13 @@ import java.nio.file.Path; // Contract that every schematic format parser must implement public interface SchematicReader { - // Returns true if this reader can handle the given file (based on extension — no I/O) + // Returns true if this reader can handle the given file (based on extension - no I/O) boolean supports(Path file); - // Reads only the metadata (dimensions, counts) — no block data + // Reads only the metadata (dimensions, counts) - no block data SchematicMetadata readMetadata(Path file) throws IOException; - // Opens a block-by-block stream for paste — not implemented until Milestone 3 + // Opens a block-by-block stream for paste - not implemented until Milestone 3 BlockStream openBlockStream(Path file, SchematicReadOptions options) throws IOException; } ``` @@ -551,7 +551,7 @@ public class SpongeSchematicReader implements SchematicReader { @Override public boolean supports(Path file) { - // No file I/O — just check the extension + // No file I/O - just check the extension return file.getFileName().toString().endsWith(".schem"); } @@ -567,7 +567,7 @@ public class SpongeSchematicReader implements SchematicReader { + " bytes (limit: " + maxCompressedBytes + ")"); } - // TODO: replace with bounded streaming — NbtIo.readCompressed loads the full NBT tree + // TODO: replace with bounded streaming - NbtIo.readCompressed loads the full NBT tree CompoundTag tag = NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap()); if (!tag.contains("Version", Tag.TAG_INT)) { @@ -623,7 +623,7 @@ public class SpongeSchematicReader implements SchematicReader { // Entities are at root in both v2 and v3 int entityCount = tag.getList("Entities", Tag.TAG_COMPOUND).size(); - // Release the entire NBT tree — we only needed the header + // Release the entire NBT tree - we only needed the header tag = null; return new SchematicMetadata( @@ -797,7 +797,7 @@ public final class SchematicLoader { } if (collected.size() > effectiveLimit) { - // We have one extra entry — that's proof there's a next page + // We have one extra entry - that's proof there's a next page hasMore = true; break outer; } @@ -868,7 +868,7 @@ import net.neoforged.neoforge.event.RegisterCommandsEvent; public class SchemCommand { - // entry point — called once per server start with the active loader instance + // entry point - called once per server start with the active loader instance public static void register(RegisterCommandsEvent event, SchematicLoader loader) { // TODO in Task 10: build the /schem command tree using Brigadier } @@ -915,7 +915,7 @@ public class FrameShift { NeoForge.EVENT_BUS.addListener(this::onServerStopping); NeoForge.EVENT_BUS.addListener(this::onRegisterCommands); - LOGGER.info("FrameShift loaded — Yet Another Schematics Manager"); + LOGGER.info("FrameShift loaded - Yet Another Schematics Manager"); } private void onServerAboutToStart(ServerAboutToStartEvent event) { @@ -1044,7 +1044,7 @@ public class SchemCommand { } if (result.nextCursor != null) { - String hint = "More results — use /schem list " + result.nextCursor; + String hint = "More results - use /schem list " + result.nextCursor; source.sendSuccess(() -> Component.literal(hint), false); } @@ -1060,7 +1060,7 @@ public class SchemCommand { // Reject names containing path separators to prevent directory traversal if (name.contains("/") || name.contains("\\") || name.contains("..")) { source.sendFailure(Component.literal( - "Invalid schematic name — path separators are not allowed.")); + "Invalid schematic name - path separators are not allowed.")); return 0; } @@ -1231,7 +1231,7 @@ Entities: 0 /schem info ../config/frameshift-server ``` -Expected: error message `Invalid schematic name — path separators are not allowed.` +Expected: error message `Invalid schematic name - path separators are not allowed.` - [ ] **Step 7: Test missing schematic** diff --git a/docs/superpowers/specs/2026-04-24-schematic-parsing-list-info-design.md b/docs/superpowers/specs/2026-04-24-schematic-parsing-list-info-design.md index b086801..cc136ed 100644 --- a/docs/superpowers/specs/2026-04-24-schematic-parsing-list-info-design.md +++ b/docs/superpowers/specs/2026-04-24-schematic-parsing-list-info-design.md @@ -2,7 +2,7 @@ **Date:** 2026-04-24 **Milestone:** 1 (Parsing layer) + 2 (Read-only commands) -**Status:** Approved — ready for implementation +**Status:** Approved - ready for implementation --- @@ -10,7 +10,7 @@ FrameShift needs to read schematic files from disk and expose them to server operators. This milestone implements: 1. A parsing abstraction layer (reader interface + Sponge implementation) -2. Metadata-only reads (dimensions, format, file size — no block data) +2. Metadata-only reads (dimensions, format, file size - no block data) 3. `/schem list` and `/schem info` commands backed by async I/O Full block streaming (for paste) is out of scope here. The interfaces are defined now so paste can slot in without redesign. @@ -107,7 +107,7 @@ Returns `true` if the filename ends with `.schem`. No file I/O. ### `readMetadata(Path file)` -**Temporary implementation — marked with `// TODO: replace with bounded streaming`** +**Temporary implementation - marked with `// TODO: replace with bounded streaming`** Steps: 1. Assert file exists and is readable → `IOException` if not @@ -129,8 +129,8 @@ Steps: 9. `tag.getList("Entities", ...).size()` → `entityCount` - For both v2 and v3: `Entities` is at root (not inside `Blocks`) - `BlockEntities` is also at root in both versions -10. `nonAirBlocks` = `-1` (requires full block data parse — not done here) -11. Drop reference to root `CompoundTag` — must not be retained +10. `nonAirBlocks` = `-1` (requires full block data parse - not done here) +11. Drop reference to root `CompoundTag` - must not be retained 12. Return `SchematicMetadata` **What is never touched:** `Palette`, `BlockData`, `Blocks.Data`, individual `BlockEntity` NBT contents. @@ -164,13 +164,13 @@ this.readers = List.of(new SpongeSchematicReader( )); this.ioExecutor = Executors.newFixedThreadPool(config.metadataIoThreads.get()); ``` -`SpongeSchematicReader` receives only the two numeric limits it needs — it does not depend on the full config class. +`SpongeSchematicReader` receives only the two numeric limits it needs - it does not depend on the full config class. ### `shutdown()` Called on `ServerStoppingEvent`. Calls `ioExecutor.shutdown()`. ### `findReader(Path file)` -Returns `Optional` — first reader where `supports(file)` is true. +Returns `Optional` - first reader where `supports(file)` is true. ### `readMetadataAsync(Path file)` ```java @@ -184,13 +184,13 @@ public CompletableFuture listAsync(Path serverRoot, int lim ``` - Resolves each entry in `config.schematicDirectories` relative to `serverRoot` -- **Shallow traversal only** — `Files.list(dir)` (one level), no recursion +- **Shallow traversal only** - `Files.list(dir)` (one level), no recursion - Skips directories that do not exist (no error) - Skips files where `findReader` returns empty (`skipped++`) - Calls `readMetadata` for each candidate; on failure logs warning and increments `failed` - **Rate-limited logging:** logs first 5 failures verbosely, then summarises the rest as `"... N more files failed, see debug log"` - Files within each directory are sorted alphabetically by filename stem before pagination -- Cursor is the filename stem of the last returned entry — `listAsync` resumes from the first entry strictly after that name (alphabetical, case-insensitive) +- Cursor is the filename stem of the last returned entry - `listAsync` resumes from the first entry strictly after that name (alphabetical, case-insensitive) - Returns at most `limit` entries (bounded by `config.maxListResults`) - Runs entirely on `ioExecutor` @@ -228,7 +228,7 @@ These are read at `SchematicLoader` construction time. Changes require a server ### `/schem list [cursor]` - Permission: level 2 (OP) -- `cursor`: optional `StringArgumentType.word()` — filename stem of last seen entry +- `cursor`: optional `StringArgumentType.word()` - filename stem of last seen entry - Dispatches `loader.listAsync(serverRoot, maxListResults, cursor)` on IO executor - Result returned to server thread via `.thenAcceptAsync(result -> { ... }, server::execute)` - Errors returned to server thread via: @@ -240,13 +240,13 @@ These are read at `SchematicLoader` construction time. Changes require a server ``` - Return values: `1` on success, `0` on failure - Output format per entry: `name WxHxL filesize` -- Footer: if `nextCursor != null`, shows `"More results — use /schem list "` with the cursor value +- Footer: if `nextCursor != null`, shows `"More results - use /schem list "` with the cursor value - Shows skipped/failed counts if non-zero ### `/schem info ` - Permission: level 2 (OP) -- ``: `StringArgumentType.greedyString()` — filename stem (no extension), searched across configured directories in order +- ``: `StringArgumentType.greedyString()` - filename stem (no extension), searched across configured directories in order - If name contains path separators, reject with a clear error - Resolves to first matching file found across directories - Dispatches `loader.readMetadataAsync(path)` on IO executor