VITAL TOOLCHAIN CHAIN FOR MIGRATION (SUPERPOWERS)

This commit is contained in:
bigguy345 committed 2026-03-16 00:48:04 +02:00
1 parent ea8235320e
commit f3f800e166
24 files changed
+95310 -18

No files matched your search

+18 -18
View File
@@ -21,23 +21,23 @@ Phase 3 ░░░░░░░░░░░░░░░░████ Feature pa
**Goal:** Get the build system working for multi-version and merge the scripting API into platform-api.
#### 0A. Composite Build Setup
- [x] Create `mc1710/` directory, move current `src/main/java/` and `src/main/resources/` into it
- [x] Create `mc1710/settings.gradle` and `mc1710/build.gradle` with RFG
- [x] Update root `settings.gradle` to use `includeBuild 'mc1710'` instead of direct source
- [x] Verify `mc1710/` builds and runs identically to current root build
- [x] Verify `platform-api` and `core` are consumed correctly via composite build dependency
- [ ] Create `mc1710/` directory, move current `src/main/java/` and `src/main/resources/` into it
- [ ] Create `mc1710/settings.gradle` and `mc1710/build.gradle` with RFG
- [ ] Update root `settings.gradle` to use `includeBuild 'mc1710'` instead of direct source
- [ ] Verify `mc1710/` builds and runs identically to current root build
- [ ] Verify `platform-api` and `core` are consumed correctly via composite build dependency
**Scope:** ~5-10 files (build configs only). No Java changes.
**Risk:** Medium — build system restructure can surface classpath/dependency issues.
**Verification:** `./gradlew build` succeeds, `runClient` launches, existing tests pass.
#### 0B. Scripting API → platform-api Merge (Phase 1 of API merge: MC-free interfaces)
- [x] Identify the ~131 MC-free interfaces in `src/api/java/` (no `net.minecraft.*` or `cpw.mods.fml.*` imports)
- [x] Move them into `platform-api/` retaining exact package structure (`noppes.npcs.api.*`)
- [x] Reconcile the 30 existing split-package shadows (platform-api already has MC-free versions of these)
- [x] Strip generic type parameters from 5 entity interfaces (`IEntity`, `IEntityLivingBase`, `IEntityLiving`, `ICustomNpc`, `IPlayer`)
- [x] Update `core/` imports if any were pointing at the old location
- [x] Update `mc1710/` to shadow the moved interfaces where MC-specific extensions are needed
- [ ] Identify the ~131 MC-free interfaces in `src/api/java/` (no `net.minecraft.*` or `cpw.mods.fml.*` imports)
- [ ] Move them into `platform-api/` retaining exact package structure (`noppes.npcs.api.*`)
- [ ] Reconcile the 30 existing split-package shadows (platform-api already has MC-free versions of these)
- [ ] Strip generic type parameters from 5 entity interfaces (`IEntity`, `IEntityLivingBase`, `IEntityLiving`, `ICustomNpc`, `IPlayer`)
- [ ] Update `core/` imports if any were pointing at the old location
- [ ] Update `mc1710/` to shadow the moved interfaces where MC-specific extensions are needed
**Scope:** ~131 file moves + 30 reconciliations + 5 generics strips.
**Risk:** Low-Medium — package structure preserved, no behavioral changes.
@@ -45,13 +45,13 @@ Phase 3 ░░░░░░░░░░░░░░░░████ Feature pa
**Dependency:** Phase 0A must be complete (mc1710 is a separate build).
#### 0C. Scripting API → platform-api Merge (Phase 2: MC-contaminated interfaces)
- [x] Identify 71 MC-contaminated interfaces in `src/api/java/`
- [x] For each, replace MC type references with platform-api equivalents or Object returns
- [x] Handle event interfaces: create platform `@Cancelable` annotation (or use the planned custom annotation parser)
- [x] Refactor `AbstractNpcAPI` — split into MC-free interface in platform-api + mc1710 implementation class
- [x] Move cleaned interfaces to `platform-api/`
- [x] Drop the 5 vendored MC class stubs (`net/minecraft/` in api/)
- [x] Update all `mc1710/` code importing these interfaces
- [ ] Identify 71 MC-contaminated interfaces in `src/api/java/`
- [ ] For each, replace MC type references with platform-api equivalents or Object returns
- [ ] Handle event interfaces: create platform `@Cancelable` annotation (or use the planned custom annotation parser)
- [ ] Refactor `AbstractNpcAPI` — split into MC-free interface in platform-api + mc1710 implementation class
- [ ] Move cleaned interfaces to `platform-api/`
- [ ] Drop the 5 vendored MC class stubs (`net/minecraft/` in api/)
- [ ] Update all `mc1710/` code importing these interfaces
**Scope:** 71 files, each needs individual review and MC-stripping.
**Risk:** Medium-High — method signature changes ripple through 276 files in mc1710 that import the API (743 import sites).
File diff suppressed because it is too large. Load diff
+737
View File
@@ -0,0 +1,737 @@
# Abstraction Superpowers — Brainstorm
> Design document for the next generation of migration tools.
> Goal: Intelligent, modular abstraction generation that agents feed parameters to and get production-ready Java files out.
---
## The Problem
When migrating a class to `core/`, every MC type it touches needs a platform-api abstraction. Today this means:
1. Agent manually writes an `IFoo` interface in platform-api
2. Agent manually writes a `FooWrapper` in mc1710 that delegates to the MC class
3. Agent runs `Create-And-Propagate` to swap types across core/
Step 1-2 is the bottleneck. For a trivial 3-method interface it's fine. For something like `IPlayer` (100+ methods) or `IWorld` (50+ methods), it's hours of manual typing — and agents get it wrong (forgetting methods, wrong signatures, MC types leaking through).
**What we need:** Tools that take a description of what to abstract and **generate both the interface AND the wrapper** — correct, complete, matching existing patterns, in seconds.
---
## The Key Insight: You Already Have The Spec
The project has **three existing sources** for what methods an abstraction should include:
### Source 1: Existing Scripting API Interfaces
`src/api/java/noppes/npcs/api/` has 48+ interfaces (`IPlayer`, `IEntity`, `IWorld`, `ICustomNpc`, etc.) that are **already curated, already clean, already tested**. These are being merged into platform-api. They define exactly which methods matter and use clean types (no MC leakage except `getMCEntity() → Object`).
**This is the 90% case.** Most abstractions already have an API interface. The tool just needs to read it and generate the wrapper.
### Source 2: Usage in core/
When a class in `core/` calls `player.getDisplayName()`, that tells us `getDisplayName()` must exist on `IPlayer`. Scanning core/ for method calls on MC types gives the **minimal required surface** — only what's actually needed.
### Source 3: Agent Judgment
Sometimes you need methods that aren't in the API yet and aren't used in core/ yet, but will be needed for future waves. The agent provides these as additions.
**Design principle:** Tools should be able to pull from ALL THREE sources, merge them, and generate from the result.
---
## Approach Comparison
### Approach A: "Interface-First" (No Blueprint File)
The interface **IS** the spec. Agent writes (or copies) the interface, then a tool reads it and generates the wrapper.
```
Agent writes IFoo.java → Generate-Wrapper reads it → FooWrapper.java
```
**Pros:**
- No intermediate format. The interface is the single source of truth.
- Agent can use normal Java IDE features to author the interface.
- The interface is production code, not a throwaway artifact.
**Cons:**
- No way to express MC-side metadata (which MC method to delegate to, type conversions).
- Works great when method names match 1:1. Falls apart for `getName()` → `getCommandSenderName()`.
- Can't express wrapper constructor patterns or field types.
**Verdict:** Great for simple cases. Needs an escape hatch for complex mappings.
---
### Approach B: "Blueprint JSON" (Intermediate Spec File)
A JSON file describes both sides: the interface and the wrapper, including method mappings and type conversions.
```
Blueprint.json → Generate-Interface → IFoo.java
Blueprint.json → Generate-Wrapper → FooWrapper.java
```
**Pros:**
- Full control over everything: method names, mappings, type transforms, custom delegation bodies.
- Can be auto-generated from existing API interfaces or usage scans.
- Agent can review/edit the JSON before generation.
- Composable: merge multiple blueprints.
**Cons:**
- Another file format to maintain.
- JSON is verbose for Java method signatures.
- Risk of the blueprint drifting from the actual generated code.
**Verdict:** Most powerful, but most complex. Best for large abstractions (IPlayer, IWorld).
---
### Approach C: "Hybrid" (Interface + Mapping File)
Agent writes the interface. A small mapping file handles the exceptions. Tool reads both.
```
IFoo.java + foo-mappings.json → Generate-Wrapper → FooWrapper.java
```
The mapping file only contains **overrides** — methods where the MC delegation isn't obvious:
```json
{
"mcClass": "net.minecraft.entity.player.EntityPlayerMP",
"fieldName": "player",
"extends": "ScriptLivingBase",
"mappings": {
"getName": { "delegate": "getCommandSenderName" },
"getHeldItem": { "transform": "return NpcAPI.getIItemStack({{mc}}.getCurrentEquippedItem())" },
"getPosition": { "transform": "return new ScriptBlockPos({{mc}}.posX, {{mc}}.posY, {{mc}}.posZ)" }
}
}
```
Methods NOT in the mapping file get default delegation: `return this.player.methodName(args)`.
**Pros:**
- Interface is real production code (not generated artifact).
- Mapping file is tiny — only the exceptions.
- 80% of methods need no mapping (same name, same types).
- Easy for agents to reason about.
**Cons:**
- Two files to coordinate (but the mapping file is optional — only needed for non-trivial cases).
- Still need to decide where mapping files live (probably `tools/migration/mappings/`).
**Verdict:** Best balance of power and simplicity. Recommended.
---
### Approach D: "Usage-Driven Auto-Generation"
Fully automated: scan core/ for what methods are called on `EntityPlayer`, auto-generate `IPlayer` with exactly those methods, auto-generate `PlayerWrapper`.
```
Scan-Usage "EntityPlayer" → auto-generate IPlayer + PlayerWrapper
```
**Pros:**
- Zero manual work. Truly a "superpower."
- Minimal surface area — only abstracts what's needed.
**Cons:**
- Method signatures can't always be inferred from call sites (especially return types).
- Generates a minimal interface that may be too narrow for future use.
- Can't handle type conversions without a mapping.
- Fragile: regex-based Java parsing will miss edge cases.
**Verdict:** Great as a DISCOVERY tool (tells you what to abstract), poor as a GENERATION tool on its own. Best combined with Approach B or C.
---
## Recommended Architecture: Hybrid Pipeline
```
┌─────────────────────────────────────────────────────┐
│ DISCOVERY PHASE │
│ │
│ Scan-Usage.ps1 ──→ usage-report.json │
│ Extract-API.ps1 ──→ methods from existing API │
│ Agent knowledge ──→ additional methods │
│ │
│ ↓ ↓ ↓ (all feed into) ↓ ↓ ↓ │
├─────────────────────────────────────────────────────┤
│ GENERATION PHASE │
│ │
│ Generate-Interface.ps1 │
│ Input: name, package, extends, method list │
│ Output: IFoo.java in platform-api/ │
│ │
│ Generate-Wrapper.ps1 │
│ Input: interface file + optional mappings │
│ Output: FooWrapper.java in mc1710/ │
│ │
├─────────────────────────────────────────────────────┤
│ PROPAGATION PHASE │
│ │
│ Symbol-Swap.ps1 (existing) │
│ Create-And-Propagate.ps1 (existing) │
│ Dedupe-Imports.ps1 (existing) │
│ Build-Report.ps1 (existing) │
└─────────────────────────────────────────────────────┘
```
---
## Proposed Tool Suite
### Tool 1: `Scan-Usage.ps1` — Usage Discovery
Scans a directory for all method calls on a given type. Answers: "What do we actually need to abstract?"
```powershell
Scan-Usage -Type "EntityPlayer" -SearchDir "core/src/main/java"
```
**Output:** JSON report
```json
{
"type": "EntityPlayer",
"searchDir": "core/src/main/java",
"methodCalls": [
{
"method": "getCommandSenderName",
"files": ["controllers/DialogController.java", "controllers/QuestController.java"],
"count": 4
},
{
"method": "getUniqueID",
"files": ["controllers/PlayerDataController.java"],
"count": 2
},
{
"method": "inventory",
"files": ["controllers/data/QuestData.java"],
"count": 1,
"note": "field access"
}
],
"fieldAccesses": [
{ "field": "posX", "files": ["..."], "count": 3 },
{ "field": "worldObj", "files": ["..."], "count": 7 }
],
"totalFiles": 12,
"totalReferences": 34
}
```
**How it works:**
- Regex scan for `variableName.methodName(` where variable type matches the target
- Also catches field access patterns
- Groups by method name, counts occurrences, lists files
- Agent uses this to decide the whitelist for Generate-Interface
---
### Tool 2: `Extract-API.ps1` — Blueprint from Existing Interface
Reads an existing Java interface file and extracts a structured method list.
```powershell
Extract-API -Source "src/api/java/noppes/npcs/api/entity/IPlayer.java"
# OR
Extract-API -Source "platform-api/src/main/java/noppes/npcs/api/entity/IEntity.java"
```
**Output:** JSON method list
```json
{
"source": "platform-api/.../IEntity.java",
"name": "IEntity",
"package": "noppes.npcs.api.entity",
"extends": [],
"imports": ["noppes.npcs.api.INbt", "noppes.npcs.api.IPos", "..."],
"methods": [
{
"name": "getX",
"returns": "double",
"params": [],
"javadoc": "The entity's x position."
},
{
"name": "setPosition",
"returns": "void",
"params": [
{ "type": "double", "name": "x" },
{ "type": "double", "name": "y" },
{ "type": "double", "name": "z" }
],
"javadoc": "Sets the entity's position to the specified coordinates."
},
{
"name": "dropItem",
"returns": "void",
"params": [{ "type": "IItemStack", "name": "item" }],
"javadoc": "Causes the entity to drop the given item."
}
],
"methodCount": 64
}
```
**Use cases:**
- Feed into Generate-Wrapper to create the mc1710 delegation class
- Compare with Scan-Usage output to find gaps
- Subset: `Extract-API -Source "..." -Whitelist "getX,getY,getZ,setPosition,getPosition"`
---
### Tool 3: `Generate-Interface.ps1` — Interface Generator
Takes a method specification and generates a production-ready Java interface file.
```powershell
Generate-Interface `
-Name "IPlayer" `
-Package "noppes.npcs.api.entity" `
-Extends @("IEntityLivingBase") `
-Methods $methodList `
-OutputDir "platform-api/src/main/java/noppes/npcs/api/entity"
```
**The `$methodList` format** (PowerShell array of hashtables):
```powershell
$methodList = @(
@{ name='getName'; returns='String'; params=@() },
@{ name='getHealth'; returns='float'; params=@() },
@{ name='setHealth'; returns='void'; params=@(
@{ type='float'; name='health' }
)},
@{ name='getHeldItem'; returns='IItemStack'; params=@() },
@{ name='setPosition'; returns='void'; params=@(
@{ type='double'; name='x' },
@{ type='double'; name='y' },
@{ type='double'; name='z' }
)},
@{ name='dropItem'; returns='void'; params=@(
@{ type='IItemStack'; name='item' }
)}
)
```
**Generated output:** Clean Java interface with:
- Correct package declaration
- Auto-detected imports (scans return types and param types for known PA types)
- Javadoc on each method (if provided in the hashtable)
- Proper formatting matching existing platform-api style
**Why this matters:** Agent provides a LIST OF METHODS and gets a file. No hand-writing boilerplate. The tool handles package, imports, formatting.
---
### Tool 4: `Generate-Wrapper.ps1` — Wrapper Skeleton Generator
Takes an interface file (or method list) and generates a wrapper class that delegates to an MC object.
```powershell
Generate-Wrapper `
-Interface "platform-api/src/main/java/noppes/npcs/api/entity/IPlayer.java" `
-MCClass "net.minecraft.entity.player.EntityPlayerMP" `
-WrapperName "PlayerWrapper" `
-WrapperPackage "noppes.npcs.scripted.entity" `
-FieldName "player" `
-ExtendsWrapper "ScriptLivingBase" `
-Mappings $mappings `
-OutputDir "mc1710/src/main/java/noppes/npcs/scripted/entity"
```
**The `$mappings` parameter** (optional — only for non-trivial delegations):
```powershell
$mappings = @{
# Method name maps to different MC method
'getName' = @{ delegate = 'getCommandSenderName' }
# Method needs type conversion
'getHeldItem' = @{ transform = 'return NpcAPI.getIItemStack(this.player.getCurrentEquippedItem())' }
# Method needs complex body
'getPosition' = @{ body = @'
return new ScriptBlockPos(this.player.posX, this.player.posY, this.player.posZ);
'@ }
# Method should be a TODO (too complex for auto-gen)
'getSurroundingEntities' = @{ todo = 'Needs entity list wrapping with NpcAPI.getIEntity()' }
}
```
**Generated output:**
```java
package noppes.npcs.scripted.entity;
import net.minecraft.entity.player.EntityPlayerMP;
import noppes.npcs.api.entity.IPlayer;
// ... auto-detected imports ...
public class PlayerWrapper extends ScriptLivingBase<EntityPlayerMP> implements IPlayer {
public PlayerWrapper(EntityPlayerMP player) {
super(player);
}
@Override
public String getName() {
return this.entity.getCommandSenderName(); // from mappings
}
@Override
public float getHealth() {
return this.entity.getHealth(); // auto-delegated (same name)
}
@Override
public void setHealth(float health) {
this.entity.setHealth(health); // auto-delegated
}
@Override
public IItemStack getHeldItem() {
return NpcAPI.getIItemStack(this.player.getCurrentEquippedItem()); // from mappings
}
@Override
public void setPosition(double x, double y, double z) {
this.entity.setPosition(x, y, z); // auto-delegated
}
@Override
public IEntity[] getSurroundingEntities(int range, int type) {
// TODO: Needs entity list wrapping with NpcAPI.getIEntity()
throw new UnsupportedOperationException("Not yet implemented");
}
}
```
**Delegation rules (applied automatically):**
1. If method name is in `$mappings` with `delegate` → use that MC method name
2. If method name is in `$mappings` with `transform` → use verbatim, substituting `{{mc}}` for field name
3. If method name is in `$mappings` with `body` → paste the body verbatim
4. If method name is in `$mappings` with `todo` → generate TODO + throw
5. **Default:** `return this.{field}.{methodName}({args})` (same name, pass args through)
---
### Tool 5: `Dependency-Map.ps1` — Migration Readiness Scanner
Answers: "What MC types does this class depend on, and which ones already have PA abstractions?"
```powershell
Dependency-Map -File "mc1710/src/main/java/noppes/npcs/controllers/QuestController.java"
```
**Output:**
```
╔══════════════════════════════════════════════════════════════════╗
║ Dependency Map: QuestController.java ║
╠══════════════════════════════════════════════════════════════════╣
║ ║
║ ✅ ABSTRACTED (ready to swap) ║
║ ├── NBTTagCompound → INbt (platform-api) ║
║ ├── EntityPlayer → IPlayer (platform-api) ║
║ ├── EntityPlayerMP → IPlayer (platform-api) ║
║ └── ItemStack → IItemStack (platform-api) ║
║ ║
║ ⚠️ NEEDS ABSTRACTION (create before migrating) ║
║ ├── MinecraftServer (3 usages: getPlayerList, getWorld) ║
║ └── ChatComponentText (1 usage: new ChatComponentText(msg)) ║
║ ║
║ 🔴 MC-COUPLED (cannot abstract — stays in mc1710 shadow) ║
║ ├── EntityNPCInterface (extends Entity) ║
║ └── WorldServer (needs IWorld extension) ║
║ ║
║ 📊 Migration readiness: 4/8 types abstracted (50%) ║
║ 📋 Blocking: 2 types need new PA interfaces ║
╚══════════════════════════════════════════════════════════════════╝
```
**How it works:**
- Parse imports from the file
- Check each import against known PA type mappings (hardcoded table + scan of platform-api/)
- Categorize: abstracted / needs abstraction / mc-coupled
- For "needs abstraction" — scan the file for what methods are called on that type
**Use case:** Before starting a migration wave, run this on every file you plan to migrate. It tells you what PA interfaces to create first.
---
### Tool 6: `Batch-Plan.ps1` — Migration Wave Planner
Given a list of files to migrate, computes the optimal order and required abstractions.
```powershell
Batch-Plan -Files @(
'noppes/npcs/controllers/QuestController.java',
'noppes/npcs/controllers/PlayerQuestController.java',
'noppes/npcs/quests/QuestItem.java',
'noppes/npcs/quests/QuestKill.java'
)
```
**Output:**
```
═══ Migration Wave Plan ═══
Step 1: Create PA Interfaces
└── IServerService (needed by: QuestController)
└── IMessageService (needed by: QuestController, PlayerQuestController)
Step 2: Copy Files (ordered by dependency depth)
1. QuestItem.java (0 internal deps)
2. QuestKill.java (0 internal deps)
3. PlayerQuestController (depends on: QuestItem, QuestKill)
4. QuestController.java (depends on: PlayerQuestController, QuestItem, QuestKill)
Step 3: Stubs Needed
└── SyncController.updateQuest()
└── ScriptController.HasStart
└── EventHooks (empty)
Step 4: Swaps
└── NBTTagCompound → INbt (12 occurrences across 4 files)
└── EntityPlayer → IPlayer (8 occurrences across 3 files)
└── MinecraftServer → IServerService (3 occurrences in 1 file)
```
---
## Type Mapping Registry
Instead of hardcoding type mappings in every script, maintain a single registry file: `tools/migration/type-registry.json`
```json
{
"mappings": [
{
"mcType": "NBTTagCompound",
"mcImport": "net.minecraft.nbt.NBTTagCompound",
"paType": "INbt",
"paImport": "noppes.npcs.api.INbt",
"status": "done"
},
{
"mcType": "EntityPlayer",
"mcImport": "net.minecraft.entity.player.EntityPlayer",
"paType": "IPlayer",
"paImport": "noppes.npcs.api.entity.IPlayer",
"wrapMethod": "PlatformServiceHolder.get().wrapPlayer({{mc}})",
"status": "done"
},
{
"mcType": "World",
"mcImport": "net.minecraft.world.World",
"paType": "IWorld",
"paImport": "noppes.npcs.api.IWorld",
"wrapMethod": "PlatformServiceHolder.get().wrapWorld({{mc}})",
"status": "done"
},
{
"mcType": "MinecraftServer",
"mcImport": "net.minecraft.server.MinecraftServer",
"paType": null,
"paImport": null,
"status": "not-abstracted"
}
]
}
```
**All tools read from this registry:**
- `Dependency-Map` uses it to categorize deps as abstracted/not-abstracted
- `Generate-Wrapper` uses `wrapMethod` to auto-generate type conversions in delegation
- `Symbol-Swap` can auto-generate `-Swaps` from the registry
- `Batch-Plan` knows which abstractions already exist
**When you create a new PA interface, you add ONE entry to the registry.** Every tool instantly knows about it.
---
## Workflow Examples
### Workflow A: Migrate a controller that needs a new abstraction
```powershell
# 1. Check what QuestController depends on
Dependency-Map -File "mc1710/.../QuestController.java"
# Output: MinecraftServer needs abstraction, 3 usages
# 2. Scan what methods are called on MinecraftServer in core/
Scan-Usage -Type "MinecraftServer" -SearchDir "core/src/main/java"
# Output: getPlayerList() x2, getEntityWorld() x1
# 3. Generate the interface (agent provides method list based on scan)
Generate-Interface -Name "IServerService" -Package "noppes.npcs.api.handler" `
-Methods @(
@{ name='getPlayerList'; returns='Object'; params=@() },
@{ name='getEntityWorld'; returns='IWorld'; params=@(
@{ type='int'; name='dimensionId' }
)}
) `
-OutputDir "platform-api/.../noppes/npcs/api/handler"
# 4. Generate the wrapper
Generate-Wrapper -Interface "platform-api/.../IServerService.java" `
-MCClass "net.minecraft.server.MinecraftServer" `
-WrapperName "ServerServiceWrapper" `
-OutputDir "mc1710/.../noppes/npcs/scripted"
# 5. Propagate (existing tools)
Symbol-Swap -Directory "core/src/main/java" -Swaps @(
,@('MinecraftServer', 'IServerService', 'net.minecraft.server.MinecraftServer', 'noppes.npcs.api.handler.IServerService')
)
```
### Workflow B: Port an existing API interface to platform-api
```powershell
# 1. Extract method list from existing scripting API interface
$api = Extract-API -Source "src/api/java/noppes/npcs/api/entity/IEntityLiving.java"
# Output: JSON with 15 methods, all clean types
# 2. The interface already exists in platform-api (merged) — just generate the wrapper
Generate-Wrapper `
-Interface "platform-api/.../IEntityLiving.java" `
-MCClass "net.minecraft.entity.EntityLiving" `
-WrapperName "ScriptLiving" `
-FieldName "entity" `
-ExtendsWrapper "ScriptLivingBase" `
-Mappings @{
'getNavigator' = @{ todo = 'Need INavigator abstraction first' }
} `
-OutputDir "mc1710/.../noppes/npcs/scripted/entity"
```
### Workflow C: Full automated wave
```powershell
# 1. Plan the wave
$plan = Batch-Plan -Files @('controllers/QuestController.java', '...')
# 2. Create missing abstractions (from plan output)
foreach ($needed in $plan.NewAbstractions) {
# Agent provides method lists
Generate-Interface -Name $needed.Name -Package $needed.Package -Methods $needed.Methods
Generate-Wrapper -Interface $needed.InterfacePath -MCClass $needed.MCClass ...
}
# 3. Copy, nuke, swap, stub, build (existing pipeline)
Copy-Recursive -Sources $plan.Files -AllowPaths $plan.AllowPaths
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes $standardPrefixes
Symbol-Swap -Directory 'core/src/main/java' -Swaps $plan.Swaps
Generate-Stubs -Stubs $plan.Stubs
Dedupe-Imports -Directory 'core/src/main/java'
Build-Report -GradleTask ':core:compileJava'
```
---
## Open Questions
### Q1: Where do wrappers live?
Current pattern: `mc1710/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java`
AGENTS.md says new wrappers should be `[Thing]Wrapper` (e.g., `PlayerWrapper`).
**Options:**
- A) Keep `scripted/entity/` path, use `ScriptXxx` naming (consistent with existing)
- B) New `mc1710/.../noppes/npcs/wrapper/` package, use `XxxWrapper` naming (cleaner)
- C) Agent decides per-case
### Q2: How deep should auto-delegation go?
For `IItemStack getHeldItem()`, the wrapper needs:
```java
return NpcAPI.getIItemStack(this.player.getCurrentEquippedItem());
```
Should Generate-Wrapper auto-detect that `IItemStack` return type needs wrapping (from the type registry), or should the agent always provide a mapping for type-converting methods?
**Options:**
- A) Auto-detect from type registry (smart but fragile)
- B) Agent always provides mappings for non-primitive returns (explicit but verbose)
- C) Auto-detect with agent override (best of both, most complex to implement)
### Q3: Should we generate Javadoc?
Existing platform-api interfaces have full Javadoc on every method (see IEntity.java — 604 lines for 64 methods). Should the generator:
- A) Copy Javadoc from source API interface if available
- B) Generate placeholder `/** TODO */`
- C) Skip Javadoc (agent adds later)
### Q4: Composable method "trait packs"?
Instead of listing methods individually, define reusable groups:
```powershell
$POSITION_METHODS = @(
@{ name='getX'; returns='double'; params=@() },
@{ name='getY'; returns='double'; params=@() },
@{ name='getZ'; returns='double'; params=@() },
@{ name='setPosition'; returns='void'; params=@(
@{ type='double'; name='x' },
@{ type='double'; name='y' },
@{ type='double'; name='z' }
)}
)
$HEALTH_METHODS = @(
@{ name='getHealth'; returns='float'; params=@() },
@{ name='setHealth'; returns='void'; params=@(@{ type='float'; name='health' }) },
@{ name='isAlive'; returns='boolean'; params=@() }
)
# Compose:
Generate-Interface -Name "IEntityLiving" -Methods ($POSITION_METHODS + $HEALTH_METHODS + $AI_METHODS)
```
Worth the complexity? Or is copy-paste fine since agents handle the composition?
### Q5: Registry format — JSON or PowerShell?
**JSON:** Universal, agents can read/edit it, but needs `ConvertFrom-Json` parsing.
**PowerShell .psd1:** Native hashtable, zero parsing, but agents are less familiar with the format.
**PowerShell .ps1 with variables:** Dot-source it, variables are live. Most ergonomic for tool consumption.
Leaning toward **JSON** — it's the lingua franca that agents handle best, and `ConvertFrom-Json` is one line.
---
## Priority Order for Implementation
| Priority | Tool | Effort | Impact |
|----------|------|--------|--------|
| **P0** | `Generate-Wrapper.ps1` | High | Eliminates the #1 bottleneck (hand-writing wrapper delegation) |
| **P0** | `Generate-Interface.ps1` | Medium | Fast interface scaffolding from method lists |
| **P1** | `Extract-API.ps1` | Medium | Reads existing API interfaces into method lists for Generate-Wrapper |
| **P1** | Type Registry (`type-registry.json`) | Low | Central truth for all MC→PA mappings |
| **P2** | `Dependency-Map.ps1` | Medium | Pre-migration readiness check |
| **P2** | `Scan-Usage.ps1` | Medium | Discovery of minimal abstraction surface |
| **P3** | `Batch-Plan.ps1` | High | Full wave planning (can be done manually for now) |
**P0 tools unlock 10x speed.** An agent that can say "generate wrapper for IPlayer against EntityPlayerMP" and get 80%+ correct code in one command — that's the superpower.
---
## Summary
The breakthrough idea: **the interface IS the spec, the type registry knows the conversions, and Generate-Wrapper reads both to produce 80-95% correct delegation code.** The remaining 5-20% (complex type conversions, multi-step logic) is handled by explicit mappings the agent provides.
No JSON blueprint files needed for most cases. The pipeline:
1. Interface exists (copied from API or generated) → **source of truth**
2. Type registry maps MC types to PA types → **auto-conversion**
3. Generate-Wrapper reads interface + registry + optional mappings → **wrapper skeleton**
4. Agent reviews, fills TODOs, fixes edge cases → **production code**
5. Existing tools propagate → **done**
File diff suppressed because it is too large. Load diff
+136
View File
@@ -0,0 +1,136 @@
# Build-Report.ps1
# =================
# SUPERWEAPON: Run a Gradle build and categorize errors into actionable patterns.
#
# Instead of dumping 500 lines of raw Gradle output, this function:
# 1. Runs the specified Gradle task
# 2. Captures all error lines
# 3. Categorizes them into patterns: MISSING_CLASS, MISSING_PACKAGE, MISSING_METHOD,
# TYPE_MISMATCH, OTHER
# 4. Outputs a structured report with counts and sample errors
#
# The agent reads the report and fixes errors one category at a time. No panic.
#
# USAGE:
# . tools/migration/Build-Report.ps1
#
# Build-Report -GradleTask ':core:compileJava'
# Build-Report -GradleTask ':core:compileJava' -MaxSamples 5
#
# PARAMS:
# -GradleTask The Gradle task to run (e.g., ':core:compileJava', 'build').
# -MaxSamples Max number of sample errors to show per category. Default: 3.
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns hashtable of categorized errors.
# Prints structured report to stdout.
function Build-Report {
param(
[Parameter(Mandatory=$true)]
[string]$GradleTask,
[int]$MaxSamples = 3,
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
Write-Host ""
Write-Host "============================================"
Write-Host " BUILD-REPORT -- Error Categorizer"
Write-Host "============================================"
Write-Host "Task: $GradleTask"
Write-Host "Root: $ProjectRoot"
Write-Host "============================================"
Write-Host ""
Write-Host "Running build..."
# Run Gradle and capture output
$gradlew = Join-Path $ProjectRoot 'gradlew.bat'
if (!(Test-Path $gradlew)) {
$gradlew = Join-Path $ProjectRoot 'gradlew'
}
$output = & cmd /c "`"$gradlew`" $GradleTask 2>&1" | Out-String
# Parse error lines
$errors = @{}
$output -split "`n" | Where-Object { $_ -match 'error:' } | ForEach-Object {
$line = $_.Trim()
if ($line -match '(?<file>[^\s:]+\.java):(?<linenum>\d+):\s*error:\s*(?<msg>.+)') {
$msg = $Matches.msg.Trim()
$file = $Matches.file
$linenum = $Matches.linenum
$category = if ($msg -match 'cannot find symbol.*symbol:\s*class\s+(\w+)') {
"MISSING_CLASS:$($Matches[1])"
} elseif ($msg -match 'package\s+(\S+)\s+does not exist') {
"MISSING_PACKAGE:$($Matches[1])"
} elseif ($msg -match 'cannot find symbol.*symbol:\s*method\s+(\w+)') {
"MISSING_METHOD:$($Matches[1])"
} elseif ($msg -match 'cannot find symbol.*symbol:\s*variable\s+(\w+)') {
"MISSING_VARIABLE:$($Matches[1])"
} elseif ($msg -match 'cannot find symbol') {
"MISSING_SYMBOL"
} elseif ($msg -match 'incompatible types') {
"TYPE_MISMATCH"
} elseif ($msg -match 'cannot be applied') {
"BAD_ARGS"
} elseif ($msg -match 'is not abstract') {
"ABSTRACT_VIOLATION"
} elseif ($msg -match 'unreported exception') {
"UNCHECKED_EXCEPTION"
} else {
"OTHER"
}
if (!$errors[$category]) { $errors[$category] = [System.Collections.ArrayList]::new() }
[void]$errors[$category].Add("$file`:$linenum -- $msg")
}
}
# Count total
$totalErrors = 0
foreach ($cat in $errors.Keys) { $totalErrors += $errors[$cat].Count }
# Check if build succeeded
$buildPassed = $output -match 'BUILD SUCCESSFUL'
# Report
Write-Host ""
Write-Host "============================================"
Write-Host " BUILD REPORT"
Write-Host "============================================"
if ($buildPassed) {
Write-Host "BUILD SUCCESSFUL -- no errors!"
Write-Host "============================================"
return @{ Success = $true; Errors = @{}; Total = 0 }
}
Write-Host "Total errors: $totalErrors"
Write-Host ""
foreach ($cat in $errors.Keys | Sort-Object) {
$count = $errors[$cat].Count
Write-Host "--- $cat ($count occurrences) ---"
$errors[$cat] | Select-Object -First $MaxSamples | ForEach-Object {
Write-Host " $_"
}
if ($count -gt $MaxSamples) {
Write-Host " ... and $($count - $MaxSamples) more"
}
Write-Host ""
}
Write-Host "============================================"
return @{ Success = $false; Errors = $errors; Total = $totalErrors }
}
+244
View File
@@ -0,0 +1,244 @@
# Copy-Recursive.ps1
# ====================
# SUPERWEAPON: Recursive dependency-aware file copier.
#
# Given one or more Java source files from mc1710, this function:
# 1. Copies each file to the corresponding location in core/
# 2. Parses all import statements in the copied file
# 3. For each import that maps to another project file:
# - Checks if the file's path starts with an ALLOWED prefix (whitelist)
# - Checks if it already exists in core/
# - If allowed AND not in core, copies it
# - Then recursively processes THAT file's imports
# 4. Skips all Minecraft/Forge/Java stdlib imports
# 5. Logs every file it copies and every file it skips (with reason)
#
# USAGE:
# . tools/migration/Copy-Recursive.ps1
#
# # Copy AnimationController and ALL its allowed deps:
# Copy-Recursive -Sources @('noppes/npcs/controllers/AnimationController.java') `
# -AllowPaths @('noppes/npcs/controllers/', 'noppes/npcs/roles/', 'noppes/npcs/quests/', 'noppes/npcs/config/')
#
# PARAMS:
# -Sources Array of relative paths within McRoot (e.g., 'noppes/npcs/controllers/Foo.java')
# -AllowPaths WHITELIST: only recursively copy files whose path starts with one of these prefixes.
# The initial -Sources files are ALWAYS copied regardless of whitelist.
# If empty/not provided, ALL project files are allowed (no filter).
# -McRoot Source root directory (default: 'mc1710/src/main/java')
# -CoreRoot Destination root directory (default: 'core/src/main/java')
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
# -ForbiddenPrefixes Import prefixes that are NEVER followed (external deps).
# Defaults to java.*, net.minecraft.*, cpw.mods.*, net.minecraftforge.*, etc.
#
# OUTPUT:
# Returns array of all files that were copied (relative paths).
# Prints detailed log to stdout.
function Copy-Recursive {
param(
[Parameter(Mandatory=$true)]
[string[]]$Sources,
[string[]]$AllowPaths = @(),
[string]$McRoot = 'mc1710/src/main/java',
[string]$CoreRoot = 'core/src/main/java',
[string]$ProjectRoot,
# Import prefixes that are NEVER followed/copied (external deps)
[string[]]$ForbiddenPrefixes = @(
'java.', 'javax.', 'sun.',
'net.minecraft.', 'cpw.mods.', 'net.minecraftforge.',
'org.lwjgl.', 'org.apache.', 'com.google.',
'org.mozilla.', 'io.netty.', 'org.objectweb.',
'scala.', 'org.spongepowered.'
)
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
# State
$script:visited = @{}
$script:copied = [System.Collections.ArrayList]::new()
$script:skippedExisting = [System.Collections.ArrayList]::new()
$script:skippedNotAllowed = [System.Collections.ArrayList]::new()
$script:notFound = [System.Collections.ArrayList]::new()
$script:depth = 0
$hasWhitelist = $AllowPaths.Count -gt 0
function Is-Forbidden {
param([string]$Import)
foreach ($prefix in $ForbiddenPrefixes) {
if ($Import.StartsWith($prefix)) { return $true }
}
return $false
}
function Is-Allowed {
param([string]$RelativePath)
if (-not $hasWhitelist) { return $true }
foreach ($allow in $AllowPaths) {
if ($RelativePath.StartsWith($allow)) { return $true }
}
return $false
}
function Process-File {
param(
[string]$RelativePath,
[bool]$IsInitialSource = $false
)
# Normalize separators
$RelativePath = $RelativePath -replace '\\', '/'
# Avoid cycles
if ($script:visited.ContainsKey($RelativePath)) { return }
$script:visited[$RelativePath] = $true
# Whitelist check (initial sources bypass)
if (-not $IsInitialSource -and -not (Is-Allowed -RelativePath $RelativePath)) {
[void]$script:skippedNotAllowed.Add($RelativePath)
return
}
$srcFull = Join-Path $ProjectRoot (Join-Path $McRoot $RelativePath)
$dstFull = Join-Path $ProjectRoot (Join-Path $CoreRoot $RelativePath)
# Skip if already exists in core
if (Test-Path $dstFull) {
[void]$script:skippedExisting.Add($RelativePath)
# STILL parse imports to find transitive deps that may not exist in core yet
$content = [IO.File]::ReadAllText($dstFull)
$indent = ' ' * $script:depth
Write-Host "${indent}EXISTS: $RelativePath"
Parse-And-Follow -Content $content
return
}
# Skip if source doesn't exist in mc1710
if (!(Test-Path $srcFull)) {
[void]$script:notFound.Add($RelativePath)
return
}
# Copy the file
$dir = Split-Path $dstFull -Parent
if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
Copy-Item $srcFull $dstFull -Force
[void]$script:copied.Add($RelativePath)
$indent = ' ' * $script:depth
Write-Host "${indent}COPIED: $RelativePath"
# Parse imports and recurse
$content = [IO.File]::ReadAllText($srcFull)
Parse-And-Follow -Content $content
}
function Parse-And-Follow {
param([string]$Content)
$imports = [regex]::Matches($Content, 'import\s+(?:static\s+)?([^;]+);')
$script:depth++
foreach ($match in $imports) {
$imp = $match.Groups[1].Value.Trim()
# Skip forbidden (MC, Forge, Java stdlib, etc.)
if (Is-Forbidden -Import $imp) {
continue
}
# Handle wildcard imports: import noppes.npcs.controllers.data.*;
if ($imp -match '\.\*$') {
$pkgPath = ($imp -replace '\.\*$', '') -replace '\.', '/'
$pkgDir = Join-Path $ProjectRoot (Join-Path $McRoot $pkgPath)
if (Test-Path $pkgDir) {
Get-ChildItem -Path $pkgDir -Filter '*.java' -File | ForEach-Object {
$relPath = ($pkgPath + '/' + $_.Name) -replace '\\', '/'
Process-File -RelativePath $relPath
}
}
continue
}
# Convert import to file path and try to find it
$filePath = ($imp -replace '\.', '/') + '.java'
$fullCheck = Join-Path $ProjectRoot (Join-Path $McRoot $filePath)
if (Test-Path $fullCheck) {
Process-File -RelativePath $filePath
} else {
# Might be inner class or static member -- try removing last segment(s)
$parts = $imp -split '\.'
for ($i = $parts.Length - 1; $i -ge 1; $i--) {
$tryImport = ($parts[0..($i-1)] -join '/') + '.java'
$tryFull = Join-Path $ProjectRoot (Join-Path $McRoot $tryImport)
if (Test-Path $tryFull) {
Process-File -RelativePath $tryImport
break
}
}
}
}
$script:depth--
}
# ===== MAIN EXECUTION =====
Write-Host ""
Write-Host "============================================"
Write-Host " COPY-RECURSIVE -- Dependency-Aware Copier"
Write-Host "============================================"
Write-Host "Source root: $McRoot"
Write-Host "Dest root: $CoreRoot"
Write-Host "Targets: $($Sources.Count) file(s)"
if ($hasWhitelist) {
Write-Host "Whitelist: $($AllowPaths.Count) path prefix(es)"
foreach ($a in $AllowPaths) { Write-Host " >> $a" }
} else {
Write-Host "Whitelist: NONE (all project files allowed)"
}
Write-Host "============================================"
Write-Host ""
foreach ($src in $Sources) {
$src = $src -replace '\\', '/'
Process-File -RelativePath $src -IsInitialSource $true
}
# ===== SUMMARY =====
Write-Host ""
Write-Host "============================================"
Write-Host " RECURSIVE COPY SUMMARY"
Write-Host "============================================"
Write-Host "Files COPIED: $($script:copied.Count)"
foreach ($f in $script:copied) {
Write-Host " + $f"
}
Write-Host ""
Write-Host "Already in core: $($script:skippedExisting.Count)"
foreach ($f in $script:skippedExisting) {
Write-Host " = $f"
}
Write-Host ""
Write-Host "Skipped (not whitelisted): $($script:skippedNotAllowed.Count)"
foreach ($f in $script:skippedNotAllowed) {
Write-Host " x $f"
}
Write-Host ""
Write-Host "Not found in mc1710: $($script:notFound.Count)"
foreach ($f in $script:notFound) {
Write-Host " ? $f"
}
Write-Host "============================================"
return $script:copied.ToArray()
}
+142
View File
@@ -0,0 +1,142 @@
# Create-And-Propagate.ps1
# ========================
# SUPERWEAPON: Atomically create a PA interface AND replace its MC equivalent everywhere.
#
# This function:
# 1. Writes a new file (typically a platform-api interface)
# 2. Scans all .java files in a target directory
# 3. For each file, performs Symbol-Swap style replacement:
# - Replaces old type name with new type name
# - Removes old MC import
# - Adds new PA import
# 4. Creation and propagation happen in ONE call -- no partial states.
#
# USAGE:
# . tools/migration/Create-And-Propagate.ps1
#
# Create-And-Propagate `
# -FilePath 'platform-api/src/main/java/noppes/npcs/api/handler/IMessageService.java' `
# -Content $interfaceCode `
# -TargetDirectory 'core/src/main/java' `
# -Swaps @(
# @('ChatComponentText', 'IMessageService', 'net.minecraft.util.ChatComponentText', 'noppes.npcs.api.handler.IMessageService'),
# @('ChatComponentTranslation', 'IMessageService', 'net.minecraft.util.ChatComponentTranslation', 'noppes.npcs.api.handler.IMessageService')
# )
#
# PARAMS:
# -FilePath Relative path (from ProjectRoot) for the new file to create.
# -Content String content of the new file.
# -TargetDirectory Relative path (from ProjectRoot) to scan for propagation. All .java files
# in this directory (recursively) will have swaps applied.
# -Swaps Array of 4-element arrays: @(oldSymbol, newSymbol, oldImport, newImport).
# Same format as Symbol-Swap.ps1.
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns hashtable with 'Created' (file path) and 'Modified' (array of modified files).
# Prints log to stdout.
function Create-And-Propagate {
param(
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$true)]
[string]$Content,
[Parameter(Mandatory=$true)]
[string]$TargetDirectory,
[Parameter(Mandatory=$true)]
[array]$Swaps,
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
# Guard against PowerShell single-element array flattening
if ($Swaps.Count -gt 0 -and $Swaps[0] -is [string]) {
$Swaps = @(,$Swaps)
}
Write-Host ""
Write-Host "============================================"
Write-Host " CREATE-AND-PROPAGATE -- Atomic PA Creator"
Write-Host "============================================"
Write-Host "New file: $FilePath"
Write-Host "Target dir: $TargetDirectory"
Write-Host "Swaps: $($Swaps.Count)"
foreach ($s in $Swaps) {
Write-Host " $($s[0]) -> $($s[1])"
}
Write-Host "============================================"
Write-Host ""
# Step 1: Create the new file
$fullPath = Join-Path $ProjectRoot $FilePath
$dir = Split-Path $fullPath -Parent
if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
[IO.File]::WriteAllText($fullPath, $Content)
Write-Host " CREATED: $FilePath"
# Step 2: Propagate swaps across target directory
$fullTargetDir = Join-Path $ProjectRoot $TargetDirectory
if (!(Test-Path $fullTargetDir)) {
Write-Host "ERROR: Target directory not found: $fullTargetDir"
return @{ Created = $FilePath; Modified = @() }
}
$files = Get-ChildItem -Recurse -Filter '*.java' -Path $fullTargetDir
$modified = [System.Collections.ArrayList]::new()
foreach ($f in $files) {
$fileContent = [IO.File]::ReadAllText($f.FullName)
$original = $fileContent
$fileSwapped = $false
# PHASE 1: Remove all old imports FIRST (before symbol replacement mangles them)
foreach ($swap in $Swaps) {
$oldImp = $swap[2]
if ($oldImp -and ($fileContent -match "import\s+(?:static\s+)?$([regex]::Escape($oldImp))\s*;")) {
$fileContent = $fileContent -replace "import\s+(?:static\s+)?$([regex]::Escape($oldImp))\s*;\r?\n", ''
}
}
# PHASE 2: Replace symbols in code body
foreach ($swap in $Swaps) {
$oldSym = $swap[0]; $newSym = $swap[1]
if ($fileContent -match [regex]::Escape($oldSym)) {
$fileContent = $fileContent -replace [regex]::Escape($oldSym), $newSym
$fileSwapped = $true
}
}
# PHASE 3: Add new imports (if swap happened, not empty, and not already present)
foreach ($swap in $Swaps) {
$newImp = $swap[3]
if ($newImp -and $fileSwapped -and !($fileContent -match "import\s+$([regex]::Escape($newImp))\s*;")) {
$fileContent = $fileContent -replace '(package\s+[^;]+;\s*\r?\n)', "`$1import $newImp;`n"
}
}
if ($fileContent -ne $original) {
[IO.File]::WriteAllText($f.FullName, $fileContent)
[void]$modified.Add($f.FullName.Substring($fullTargetDir.Length + 1))
Write-Host " PROPAGATED: $($f.Name)"
}
}
Write-Host ""
Write-Host "============================================"
Write-Host " CREATE-AND-PROPAGATE SUMMARY"
Write-Host "============================================"
Write-Host "File created: $FilePath"
Write-Host "Files propagated: $($modified.Count) / $($files.Count)"
Write-Host "============================================"
return @{ Created = $FilePath; Modified = $modified.ToArray() }
}
+95
View File
@@ -0,0 +1,95 @@
# Dedupe-Imports.ps1
# ==================
# SUPERWEAPON: Clean up duplicate imports after Symbol-Swap operations.
#
# After Symbol-Swap runs, a file might have duplicate imports (e.g., both
# NBTTagCompound and NBTTagString map to INbt, producing two identical import lines).
# This function deduplicates and sorts all imports in every .java file.
#
# USAGE:
# . tools/migration/Dedupe-Imports.ps1
#
# Dedupe-Imports -Directory 'core/src/main/java'
#
# PARAMS:
# -Directory Relative path (from ProjectRoot) to scan for .java files recursively.
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns array of modified file paths (relative to Directory).
# Prints log to stdout.
function Dedupe-Imports {
param(
[Parameter(Mandatory=$true)]
[string]$Directory,
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
$fullDir = Join-Path $ProjectRoot $Directory
if (!(Test-Path $fullDir)) {
Write-Host "ERROR: Directory not found: $fullDir"
return @()
}
$files = Get-ChildItem -Recurse -Filter '*.java' -Path $fullDir
$modified = [System.Collections.ArrayList]::new()
$totalDupsRemoved = 0
Write-Host ""
Write-Host "============================================"
Write-Host " DEDUPE-IMPORTS -- Import Deduplicator"
Write-Host "============================================"
Write-Host "Directory: $Directory"
Write-Host "Files: $($files.Count)"
Write-Host "============================================"
Write-Host ""
foreach ($f in $files) {
$content = [IO.File]::ReadAllText($f.FullName)
# Extract all import lines (preserving static vs non-static)
$importMatches = [regex]::Matches($content, '(?m)^import\s+(?:static\s+)?[^;]+;\s*$')
if ($importMatches.Count -eq 0) { continue }
$importLines = $importMatches | ForEach-Object { $_.Value.Trim() }
$uniqueImports = $importLines | Sort-Object -Unique
# Check if there are duplicates
$dupsRemoved = $importLines.Count - $uniqueImports.Count
if ($dupsRemoved -eq 0) { continue }
# Remove all existing import lines
$noImports = [regex]::Replace($content, '(?m)^import\s+(?:static\s+)?[^;]+;\s*\r?\n', '')
# Clean up any extra blank lines left behind (max 2 consecutive)
$noImports = [regex]::Replace($noImports, '(\r?\n){3,}', "`n`n")
# Re-insert deduplicated imports after package line
$importBlock = ($uniqueImports -join "`n") + "`n"
$result = $noImports -replace '(package\s+[^;]+;\s*\r?\n)\s*', "`$1`n$importBlock`n"
if ($result -ne $content) {
[IO.File]::WriteAllText($f.FullName, $result)
[void]$modified.Add($f.FullName.Substring($fullDir.Length + 1))
$totalDupsRemoved += $dupsRemoved
Write-Host " DEDUPED ($dupsRemoved dups): $($f.Name)"
}
}
Write-Host ""
Write-Host "============================================"
Write-Host " DEDUPE-IMPORTS SUMMARY"
Write-Host "============================================"
Write-Host "Files modified: $($modified.Count) / $($files.Count)"
Write-Host "Duplicates removed: $totalDupsRemoved"
Write-Host "============================================"
return $modified.ToArray()
}
+101
View File
@@ -0,0 +1,101 @@
# Generate-Stubs.ps1
# ==================
# SUPERWEAPON: Create temporary compilation stubs for missing types.
#
# When core/ references classes that haven't been migrated yet (e.g., SyncController,
# ScriptController), the build fails. This function generates minimal placeholder
# classes so the build passes. The stubs get replaced when the real class arrives.
#
# USAGE:
# . tools/migration/Generate-Stubs.ps1
#
# Generate-Stubs -Stubs @(
# @{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
# body='public static void updateDialog(Object d) {} public static void removeDialog(int id) {}' },
# @{ pkg='noppes.npcs.controllers'; name='ScriptController'; type='class';
# body='public static boolean HasStart = false;' }
# ) -TargetRoot 'core/src/main/java'
#
# PARAMS:
# -Stubs Array of hashtables, each with:
# pkg - Package name (e.g., 'noppes.npcs.controllers')
# name - Class/interface name (e.g., 'SyncController')
# type - 'class' or 'interface'
# body - Code body (method stubs, field declarations). Can be multiline.
# -TargetRoot Relative path (from ProjectRoot) where stubs are created.
# Defaults to 'core/src/main/java'.
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns array of created file paths (relative to TargetRoot).
# Prints log to stdout.
function Generate-Stubs {
param(
[Parameter(Mandatory=$true)]
[array]$Stubs,
[string]$TargetRoot = 'core/src/main/java',
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
$created = [System.Collections.ArrayList]::new()
Write-Host ""
Write-Host "============================================"
Write-Host " GENERATE-STUBS -- Compilation Placeholders"
Write-Host "============================================"
Write-Host "Target root: $TargetRoot"
Write-Host "Stubs: $($Stubs.Count)"
Write-Host "============================================"
Write-Host ""
foreach ($s in $Stubs) {
$pkgPath = $s.pkg -replace '\.', '/'
$dir = Join-Path $ProjectRoot (Join-Path $TargetRoot $pkgPath)
if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
$filePath = Join-Path $dir "$($s.name).java"
$relativePath = "$pkgPath/$($s.name).java"
# Check if file already exists -- do NOT overwrite real classes
if (Test-Path $filePath) {
$existingContent = [IO.File]::ReadAllText($filePath)
if ($existingContent -notmatch 'TEMPORARY STUB') {
Write-Host " SKIPPED (real class exists): $relativePath"
continue
}
}
$content = @"
package $($s.pkg);
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public $($s.type) $($s.name) {
$($s.body)
}
"@
[IO.File]::WriteAllText($filePath, $content)
[void]$created.Add($relativePath)
Write-Host " STUB: $($s.pkg).$($s.name)"
}
Write-Host ""
Write-Host "============================================"
Write-Host " GENERATE-STUBS SUMMARY"
Write-Host "============================================"
Write-Host "Stubs created: $($created.Count) / $($Stubs.Count)"
Write-Host "============================================"
return $created.ToArray()
}
+94
View File
@@ -0,0 +1,94 @@
# Nuke-Imports.ps1
# =================
# SUPERWEAPON: Remove ALL forbidden imports from a directory.
#
# Scans every .java file in a directory and removes any import line
# whose package matches a forbidden prefix. Run this AFTER mass-copy,
# BEFORE Symbol-Swap, to clean the slate.
#
# USAGE:
# . tools/migration/Nuke-Imports.ps1
#
# Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
# 'net.minecraft.', 'cpw.mods.', 'net.minecraftforge.'
# )
#
# PARAMS:
# -Directory Relative path (from ProjectRoot) to scan for .java files recursively.
# -ForbiddenPrefixes Array of import prefixes to remove. ALL imports starting with these
# will be deleted. Passed as dotted package prefixes (e.g., 'net.minecraft.').
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns array of modified file paths (relative to Directory).
# Prints log to stdout.
function Nuke-Imports {
param(
[Parameter(Mandatory=$true)]
[string]$Directory,
[Parameter(Mandatory=$true)]
[string[]]$ForbiddenPrefixes,
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
$fullDir = Join-Path $ProjectRoot $Directory
if (!(Test-Path $fullDir)) {
Write-Host "ERROR: Directory not found: $fullDir"
return @()
}
# Build regex pattern from forbidden prefixes
$escapedPrefixes = $ForbiddenPrefixes | ForEach-Object { [regex]::Escape($_) }
$pattern = ($escapedPrefixes | ForEach-Object { "import\s+(?:static\s+)?$_" }) -join '|'
$files = Get-ChildItem -Recurse -Filter '*.java' -Path $fullDir
$modified = [System.Collections.ArrayList]::new()
$totalNuked = 0
Write-Host ""
Write-Host "============================================"
Write-Host " NUKE-IMPORTS -- Forbidden Import Remover"
Write-Host "============================================"
Write-Host "Directory: $Directory"
Write-Host "Files to scan: $($files.Count)"
Write-Host "Forbidden: $($ForbiddenPrefixes.Count) prefix(es)"
foreach ($p in $ForbiddenPrefixes) { Write-Host " x $p" }
Write-Host "============================================"
Write-Host ""
foreach ($f in $files) {
$content = [IO.File]::ReadAllText($f.FullName)
$original = $content
# Remove matching import lines (entire line including newline)
$nukedContent = [regex]::Replace($content, "(?m)^(?:$pattern)[^;]*;\s*\r?\n", '')
if ($nukedContent -ne $original) {
# Count how many imports were removed
$beforeCount = ([regex]::Matches($original, "(?m)^(?:$pattern)[^;]*;")).Count
$totalNuked += $beforeCount
[IO.File]::WriteAllText($f.FullName, $nukedContent)
[void]$modified.Add($f.FullName.Substring($fullDir.Length + 1))
Write-Host " NUKED $beforeCount imports: $($f.Name)"
}
}
Write-Host ""
Write-Host "============================================"
Write-Host " NUKE-IMPORTS SUMMARY"
Write-Host "============================================"
Write-Host "Files modified: $($modified.Count) / $($files.Count)"
Write-Host "Imports removed: $totalNuked"
Write-Host "============================================"
return $modified.ToArray()
}
+113
View File
@@ -0,0 +1,113 @@
# Static-Transform.ps1
# ====================
# SUPERWEAPON: Regex-based method call rewriting across all files.
#
# Unlike Symbol-Swap (which does simple name replacement), this function handles
# structural transformations where method signatures change:
# - CompressedStreamTools.readCompressed(file) -> PlatformServiceHolder.get().readCompressedNBT(file)
# - new NBTTagCompound() -> NBT.compound()
# - MathHelper.clamp_int(x, y, z) -> ValueUtil.clampInt(x, y, z)
#
# USAGE:
# . tools/migration/Static-Transform.ps1
#
# Static-Transform -Directory 'core/src/main/java' -Transforms @(
# @('CompressedStreamTools\.readCompressed\s*\(([^)]+)\)',
# 'PlatformServiceHolder.get().readCompressedNBT($1)'),
# @('new\s+NBTTagCompound\s*\(\)',
# 'NBT.compound()'),
# @('MathHelper\.clamp_int\s*\(',
# 'ValueUtil.clampInt(')
# )
#
# PARAMS:
# -Directory Relative path (from ProjectRoot) to scan for .java files recursively.
# -Transforms Array of 2-element arrays: @(regexPattern, replacement).
# - regexPattern: A .NET regex pattern matching the code to transform.
# Use capture groups ($1, $2, etc.) to preserve arguments.
# - replacement: The replacement string. Use $1, $2 for captured groups.
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns array of modified file paths (relative to Directory).
# Prints log to stdout.
function Static-Transform {
param(
[Parameter(Mandatory=$true)]
[string]$Directory,
[Parameter(Mandatory=$true)]
[array]$Transforms,
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
$fullDir = Join-Path $ProjectRoot $Directory
if (!(Test-Path $fullDir)) {
Write-Host "ERROR: Directory not found: $fullDir"
return @()
}
# Guard against PowerShell single-element array flattening
if ($Transforms.Count -gt 0 -and $Transforms[0] -is [string]) {
$Transforms = @(,$Transforms)
}
$files = Get-ChildItem -Recurse -Filter '*.java' -Path $fullDir
$modified = [System.Collections.ArrayList]::new()
$totalTransforms = 0
Write-Host ""
Write-Host "============================================"
Write-Host " STATIC-TRANSFORM -- Method Call Rewriter"
Write-Host "============================================"
Write-Host "Directory: $Directory"
Write-Host "Files: $($files.Count)"
Write-Host "Transforms: $($Transforms.Count)"
foreach ($t in $Transforms) {
# Show a truncated version of the pattern for readability
$shortPattern = if ($t[0].Length -gt 50) { $t[0].Substring(0, 50) + '...' } else { $t[0] }
Write-Host " /$shortPattern/ -> $($t[1])"
}
Write-Host "============================================"
Write-Host ""
foreach ($f in $files) {
$content = [IO.File]::ReadAllText($f.FullName)
$original = $content
$fileTransformCount = 0
foreach ($t in $Transforms) {
$pattern = $t[0]; $replacement = $t[1]
$matches = [regex]::Matches($content, $pattern)
if ($matches.Count -gt 0) {
$content = [regex]::Replace($content, $pattern, $replacement)
$fileTransformCount += $matches.Count
}
}
if ($content -ne $original) {
[IO.File]::WriteAllText($f.FullName, $content)
[void]$modified.Add($f.FullName.Substring($fullDir.Length + 1))
$totalTransforms += $fileTransformCount
Write-Host " TRANSFORMED ($fileTransformCount hits): $($f.Name)"
}
}
Write-Host ""
Write-Host "============================================"
Write-Host " STATIC-TRANSFORM SUMMARY"
Write-Host "============================================"
Write-Host "Files modified: $($modified.Count) / $($files.Count)"
Write-Host "Total transforms: $totalTransforms"
Write-Host "============================================"
return $modified.ToArray()
}
+123
View File
@@ -0,0 +1,123 @@
# Symbol-Swap.ps1
# ================
# SUPERWEAPON: Bulk type replacement with import surgery.
#
# For every .java file in a directory, this function:
# 1. Replaces ALL occurrences of old symbol names with new ones
# 2. Removes the old MC import line
# 3. Adds the new PA import line (if not already present)
# 4. Logs every file it modifies
#
# USAGE:
# . tools/migration/Symbol-Swap.ps1
#
# Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
# @('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.nbt.INbt'),
# @('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer')
# )
#
# PARAMS:
# -Directory Relative path (from ProjectRoot) to scan for .java files recursively.
# -Swaps Array of 4-element arrays: @(oldSymbol, newSymbol, oldImport, newImport).
# - oldSymbol: The type name to replace in code (e.g., 'NBTTagCompound')
# - newSymbol: The replacement type name (e.g., 'INbt')
# - oldImport: The fully-qualified old import to REMOVE (e.g., 'net.minecraft.nbt.NBTTagCompound')
# - newImport: The fully-qualified new import to ADD (e.g., 'noppes.npcs.api.nbt.INbt')
# Use '' (empty string) for newImport if no import should be added.
# -ProjectRoot Absolute project root (auto-detected from script location if not provided)
#
# OUTPUT:
# Returns array of modified file paths (relative to Directory).
# Prints log to stdout.
function Symbol-Swap {
param(
[Parameter(Mandatory=$true)]
[string]$Directory,
[Parameter(Mandatory=$true)]
[array]$Swaps,
[string]$ProjectRoot
)
# Auto-detect ProjectRoot from script location if not provided
if (-not $ProjectRoot) {
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
$fullDir = Join-Path $ProjectRoot $Directory
if (!(Test-Path $fullDir)) {
Write-Host "ERROR: Directory not found: $fullDir"
return @()
}
# Guard against PowerShell single-element array flattening:
# If Swaps is a flat array of strings (e.g., @('A','B','C','D')) instead of
# array-of-arrays, wrap it so the code treats it as one swap entry.
if ($Swaps.Count -gt 0 -and $Swaps[0] -is [string]) {
$Swaps = @(,$Swaps)
}
$files = Get-ChildItem -Recurse -Filter '*.java' -Path $fullDir
$modified = [System.Collections.ArrayList]::new()
Write-Host ""
Write-Host "============================================"
Write-Host " SYMBOL-SWAP -- Bulk Type Replacement"
Write-Host "============================================"
Write-Host "Directory: $Directory"
Write-Host "Files: $($files.Count)"
Write-Host "Swaps: $($Swaps.Count)"
foreach ($s in $Swaps) {
Write-Host " $($s[0]) -> $($s[1])"
}
Write-Host "============================================"
Write-Host ""
foreach ($f in $files) {
$content = [IO.File]::ReadAllText($f.FullName)
$original = $content
$fileSwapped = $false
# PHASE 1: Remove all old imports FIRST (before symbol replacement mangles them)
foreach ($swap in $Swaps) {
$oldImp = $swap[2]
if ($oldImp -and ($content -match "import\s+(?:static\s+)?$([regex]::Escape($oldImp))\s*;")) {
$content = $content -replace "import\s+(?:static\s+)?$([regex]::Escape($oldImp))\s*;\r?\n", ''
}
}
# PHASE 2: Replace symbols in code body
foreach ($swap in $Swaps) {
$oldSym = $swap[0]; $newSym = $swap[1]
if ($content -match [regex]::Escape($oldSym)) {
$content = $content -replace [regex]::Escape($oldSym), $newSym
$fileSwapped = $true
}
}
# PHASE 3: Add new imports (if swap happened, not empty, and not already present)
foreach ($swap in $Swaps) {
$newImp = $swap[3]
if ($newImp -and $fileSwapped -and !($content -match "import\s+$([regex]::Escape($newImp))\s*;")) {
$content = $content -replace '(package\s+[^;]+;\s*\r?\n)', "`$1import $newImp;`n"
}
}
if ($content -ne $original) {
[IO.File]::WriteAllText($f.FullName, $content)
[void]$modified.Add($f.FullName.Substring($fullDir.Length + 1))
Write-Host " SWAPPED: $($f.Name)"
}
}
Write-Host ""
Write-Host "============================================"
Write-Host " SYMBOL-SWAP SUMMARY"
Write-Host "============================================"
Write-Host "Files modified: $($modified.Count) / $($files.Count)"
Write-Host "============================================"
return $modified.ToArray()
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,931 @@
{
"_meta": {
"version": "2.0",
"generated": "2026-03-15",
"author": "Expert hand-curated classification of all 400 MC types in the CustomNPC-Plus codebase",
"purpose": "Authoritative migration mapping. Tells the toolchain exactly what to do with each MC type encountered in core/ code.",
"categories": {
"EXISTING": "Already abstracted and registered in migration tooling. Symbol-Swap ready.",
"INTERFACE": "Needs a PA interface (or PA interface exists but not yet registered). These appear as types in method signatures, fields, and locals in core/ code.",
"SERVICE": "Static utility/singleton. Methods route to PlatformService or NBTIO. Cannot be interfaces (Java 7 has no static interface methods).",
"TRANSFORM": "Handled by Static-Transform regex. Annotations, enums, or math utilities that get textually replaced.",
"SUPPRESS": "Never abstract. Client-only, networking, entity hierarchy, event system, or too niche. Code using these stays in mc1710."
},
"stats": {
"total": 400,
"EXISTING": 11,
"INTERFACE": 15,
"SERVICE": 12,
"TRANSFORM": 10,
"SUPPRESS": 352
}
},
"types": {
"__comment_existing": "=== EXISTING: Already abstracted, Symbol-Swap ready (11 types) ===",
"NBTTagCompound": {
"fqn": "net.minecraft.nbt.NBTTagCompound",
"category": "EXISTING",
"pa": "INbt",
"package": "noppes.npcs.api",
"imports": 593,
"notes": "Most-imported MC type. 4,447 method calls. Core NBT compound wrapper."
},
"EntityPlayer": {
"fqn": "net.minecraft.entity.player.EntityPlayer",
"category": "EXISTING",
"pa": "IPlayer",
"package": "noppes.npcs.api.entity",
"imports": 551,
"notes": "Core player abstraction. 180 casts, 122 instanceof across codebase."
},
"EntityPlayerMP": {
"fqn": "net.minecraft.entity.player.EntityPlayerMP",
"category": "EXISTING",
"pa": "IPlayer",
"package": "noppes.npcs.api.entity",
"imports": 304,
"notes": "Server-side player. Maps to same IPlayer as EntityPlayer."
},
"Entity": {
"fqn": "net.minecraft.entity.Entity",
"category": "EXISTING",
"pa": "IEntity",
"package": "noppes.npcs.api.entity",
"imports": 262,
"notes": "Base entity type. Used heavily in casts and instanceof."
},
"ItemStack": {
"fqn": "net.minecraft.item.ItemStack",
"category": "EXISTING",
"pa": "IItemStack",
"package": "noppes.npcs.api.item",
"imports": 228,
"notes": "Item stack wrapper. NOTE: 1.11+ changes null → ItemStack.EMPTY."
},
"EntityLivingBase": {
"fqn": "net.minecraft.entity.EntityLivingBase",
"category": "EXISTING",
"pa": "IEntityLivingBase",
"package": "noppes.npcs.api.entity",
"imports": 201,
"notes": "Living entity base. 1,113 field accesses across codebase."
},
"World": {
"fqn": "net.minecraft.world.World",
"category": "EXISTING",
"pa": "IWorld",
"package": "noppes.npcs.api",
"imports": 172,
"notes": "World access. Used for block/entity queries, spawning, etc."
},
"NBTTagList": {
"fqn": "net.minecraft.nbt.NBTTagList",
"category": "EXISTING",
"pa": "INbtList",
"package": "noppes.npcs.api",
"imports": 111,
"notes": "NBT list wrapper."
},
"DamageSource": {
"fqn": "net.minecraft.util.DamageSource",
"category": "EXISTING",
"pa": "IDamageSource",
"package": "noppes.npcs.api",
"imports": 36,
"notes": "Damage source abstraction."
},
"WorldServer": {
"fqn": "net.minecraft.world.WorldServer",
"category": "EXISTING",
"pa": "IWorld",
"package": "noppes.npcs.api",
"imports": 18,
"notes": "Server world. Maps to same IWorld as World."
},
"EntityLiving": {
"fqn": "net.minecraft.entity.EntityLiving",
"category": "EXISTING",
"pa": "IEntityLiving",
"package": "noppes.npcs.api.entity",
"imports": 18,
"notes": "AI-capable living entity."
},
"__comment_interface": "=== INTERFACE: Need PA interface for core/ migration (15 types) ===",
"ResourceLocation": {
"fqn": "net.minecraft.util.ResourceLocation",
"category": "INTERFACE",
"pa": "IResourceLocation",
"package": "noppes.npcs.api",
"imports": 145,
"pa_exists": false,
"methods": ["getResourceDomain", "getResourcePath", "toString"],
"constructors": ["IResourceLocation.of(String domain, String path)", "IResourceLocation.of(String combined)"],
"reason": "Universal asset/registry identifier. Exists in all MC versions with same semantics. Pure value object — 2 getters + toString. Factory methods go to PlatformService since Java 7 can't have static interface methods.",
"notes": "Simple to abstract. Low risk. High impact (145 imports)."
},
"AxisAlignedBB": {
"fqn": "net.minecraft.util.AxisAlignedBB",
"category": "INTERFACE",
"pa": "IBoundingBox",
"package": "noppes.npcs.api",
"imports": 59,
"pa_exists": false,
"methods": ["getMinX", "getMinY", "getMinZ", "getMaxX", "getMaxY", "getMaxZ", "expand", "offset", "intersectsWith", "isVecInside"],
"reason": "Bounding box geometry. Used in collision detection, targeting, area queries. Same concept across all versions but package changes.",
"notes": "Mostly used in entity/ability code that stays in mc1710. Only abstract if core code needs it."
},
"ICommandSender": {
"fqn": "net.minecraft.command.ICommandSender",
"category": "INTERFACE",
"pa": "ICommandSender",
"package": "noppes.npcs.api",
"imports": 47,
"pa_exists": false,
"methods": ["getCommandSenderName", "addChatMessage", "canCommandSenderUseCommand", "getEntityWorld", "getCommandSenderPosition"],
"reason": "Command execution context. Used in command system which migrates to core. 1.13+ replaces with CommandSource (Brigadier).",
"notes": "Name stays ICommandSender — already good. Will need Brigadier adapter in 1.13+ leaves."
},
"Vec3": {
"fqn": "net.minecraft.util.Vec3",
"category": "INTERFACE",
"pa": "IVector3",
"package": "noppes.npcs.api",
"imports": 43,
"pa_exists": false,
"methods": ["getX", "getY", "getZ", "add", "subtract", "normalize", "distanceTo", "lengthVector", "crossProduct", "dotProduct"],
"reason": "3D vector. Used in abilities, AI targeting, raytrace. Same concept in all versions.",
"notes": "Consider making immutable value object with static factory."
},
"ChatComponentText": {
"fqn": "net.minecraft.util.ChatComponentText",
"category": "INTERFACE",
"pa": "ITextComponent",
"package": "noppes.npcs.api",
"imports": 20,
"pa_exists": false,
"methods": ["getText", "setChatStyle", "appendSibling", "getFormattedText", "getUnformattedText"],
"reason": "Chat text. 1.7: ChatComponentText, 1.16: StringTextComponent, 1.19: Component.literal(). The PA name ITextComponent is stable across all versions.",
"notes": "ChatComponentText, ChatComponentTranslation, and IChatComponent all map to ITextComponent. Factory methods on PlatformService: createTextComponent(String), createTranslation(String key, Object... args)."
},
"ChatComponentTranslation": {
"fqn": "net.minecraft.util.ChatComponentTranslation",
"category": "INTERFACE",
"pa": "ITextComponent",
"package": "noppes.npcs.api",
"imports": 24,
"pa_exists": false,
"methods": ["getKey", "getFormatArgs"],
"reason": "Translatable chat text. Maps to same ITextComponent. Factory: PlatformService.createTranslation(key, args).",
"notes": "See ChatComponentText entry."
},
"IChatComponent": {
"fqn": "net.minecraft.util.IChatComponent",
"category": "INTERFACE",
"pa": "ITextComponent",
"package": "noppes.npcs.api",
"imports": 10,
"pa_exists": false,
"methods": ["getFormattedText", "getUnformattedText", "appendSibling", "setChatStyle", "getChatStyle"],
"reason": "Chat component interface. All chat types map to ITextComponent.",
"notes": "This IS the base interface in 1.7.10. In modern MC it's Component."
},
"ChatStyle": {
"fqn": "net.minecraft.util.ChatStyle",
"category": "INTERFACE",
"pa": "ITextStyle",
"package": "noppes.npcs.api",
"imports": 3,
"pa_exists": false,
"methods": ["setColor", "setBold", "setItalic", "setUnderlined", "setStrikethrough", "setObfuscated"],
"reason": "Text formatting style. Paired with ITextComponent.",
"notes": "Low import count but semantically necessary for complete text abstraction."
},
"MovingObjectPosition": {
"fqn": "net.minecraft.util.MovingObjectPosition",
"category": "INTERFACE",
"pa": "IRayTraceResult",
"package": "noppes.npcs.api",
"imports": 15,
"pa_exists": false,
"methods": ["getType", "getBlockPos", "getEntity", "getHitVec", "getSideHit"],
"reason": "Raytrace result. 1.7: MovingObjectPosition, 1.9+: RayTraceResult, 1.16+: HitResult. Used in abilities and targeting.",
"notes": "Inner enum MovingObjectType also needs mapping (BLOCK, ENTITY, MISS)."
},
"NBTBase": {
"fqn": "net.minecraft.nbt.NBTBase",
"category": "INTERFACE",
"pa": "INbtBase",
"package": "noppes.npcs.api",
"imports": 13,
"pa_exists": false,
"methods": ["getId", "copy"],
"reason": "Base NBT tag type. Used when working with heterogeneous NBT tag collections (e.g., iterating NBTTagList contents).",
"notes": "Minimal interface. Most code already uses INbt/INbtList directly."
},
"Potion": {
"fqn": "net.minecraft.potion.Potion",
"category": "INTERFACE",
"pa": "IPotionType",
"package": "noppes.npcs.api",
"imports": 7,
"pa_exists": false,
"methods": ["getId", "getName", "isBadEffect", "isInstant", "getStatusIconIndex"],
"reason": "Potion type registry entry. 1.7: Potion[] array, 1.9+: Registry<Potion>. Used in custom effects system.",
"notes": "Registration changes significantly between versions. Core uses id-based lookup."
},
"PotionEffect": {
"fqn": "net.minecraft.potion.PotionEffect",
"category": "INTERFACE",
"pa": "IPotionEffect",
"package": "noppes.npcs.api",
"imports": 6,
"pa_exists": false,
"methods": ["getPotionID", "getDuration", "getAmplifier", "getIsAmbient", "getIsShowParticles"],
"reason": "Active potion effect instance. Used in custom effects, ability system.",
"notes": "1.9+ changes getPotionID() to getPotion() returning registry entry."
},
"Explosion": {
"fqn": "net.minecraft.world.Explosion",
"category": "INTERFACE",
"pa": "IExplosion",
"package": "noppes.npcs.api",
"imports": 3,
"pa_exists": false,
"methods": ["getExplosivePlacedBy", "getPosition"],
"reason": "Explosion event data. Used in ability system for energy explosions.",
"notes": "Low import count. Consider if SERVICE would be simpler."
},
"Configuration": {
"fqn": "net.minecraftforge.common.config.Configuration",
"category": "INTERFACE",
"pa": "IForgeConfig",
"package": "noppes.npcs.api",
"imports": 9,
"pa_exists": false,
"methods": ["get", "getCategory", "save", "load", "hasKey", "hasCategory"],
"reason": "Forge configuration file wrapper. Config system migrates to core. 1.13+ uses TOML-based ForgeConfigSpec instead.",
"notes": "Paired with Property → IConfigProperty. Config classes in core use this."
},
"Property": {
"fqn": "net.minecraftforge.common.config.Property",
"category": "INTERFACE",
"pa": "IConfigProperty",
"package": "noppes.npcs.api",
"imports": 7,
"pa_exists": false,
"methods": ["getString", "getInt", "getDouble", "getBoolean", "getStringList", "set", "setComment"],
"reason": "Individual config property. Paired with IForgeConfig.",
"notes": "Used in all ConfigMain, ConfigClient, ConfigScript, etc. files."
},
"__comment_service": "=== SERVICE: Static utility → PlatformService/NBTIO methods (12 types) ===",
"StatCollector": {
"fqn": "net.minecraft.util.StatCollector",
"category": "SERVICE",
"service": "PlatformService",
"imports": 106,
"methods": {
"translateToLocal(String key)": "Server-side translation. Returns localized string.",
"translateToLocalFormatted(String key, Object... args)": "Server-side formatted translation."
},
"reason": "Pure static translation utility. 1.7: StatCollector, 1.12+: I18n, 1.16+: different I18n. Translation is inherently platform-specific.",
"notes": "106 imports — 3rd highest SERVICE type. High-impact abstraction."
},
"Item": {
"fqn": "net.minecraft.item.Item",
"category": "SERVICE",
"service": "PlatformService",
"imports": 72,
"methods": {
"getItemById(int id)": "Registry lookup: ID → Item. Returns IItemStack or Object.",
"getIdFromItem(Item item)": "Reverse registry lookup: Item → ID."
},
"reason": "Item registry is static. Core code only uses registry lookup methods. Item subclasses (ItemArmor, ItemSword, etc.) stay in mc1710.",
"notes": "72 imports but most are in mc1710 item classes. Only registry lookups need abstracting."
},
"MinecraftServer": {
"fqn": "net.minecraft.server.MinecraftServer",
"category": "SERVICE",
"service": "PlatformService",
"imports": 26,
"methods": {
"getServer()": "Static singleton accessor. 1.7: MinecraftServer.getServer(), 1.16+: different access.",
"isDedicatedServer()": "Server type check.",
"worldServerForDimension(int dim)": "Get world by dimension ID. Returns IWorld.",
"getConfigurationManager()": "Get player list manager."
},
"reason": "Static singleton with static getServer(). Java 7 can't have static interface methods.",
"notes": "Some methods already in PlatformService (getWorldSaveDirectory, isPhysicalClient)."
},
"EntityList": {
"fqn": "net.minecraft.entity.EntityList",
"category": "SERVICE",
"service": "PlatformService",
"imports": 25,
"methods": {
"getEntityString(Entity e)": "Get entity's registry name string.",
"createEntityByName(String name, World world)": "Spawn entity by registry name."
},
"reason": "Static entity registry. 1.9+ replaces with ForgeRegistries.ENTITIES.",
"notes": "Used in clone system and NPC spawning."
},
"FMLCommonHandler": {
"fqn": "cpw.mods.fml.common.FMLCommonHandler",
"category": "SERVICE",
"service": "PlatformService",
"imports": 25,
"methods": {
"instance().getSide()": "Get physical side (CLIENT/SERVER). Already covered by PlatformService.isPhysicalClient().",
"instance().getEffectiveSide()": "Get logical side (CLIENT/SERVER)."
},
"reason": "FML singleton for side detection. Already partially covered by PlatformService.",
"notes": "1.13+ uses FMLEnvironment.dist instead."
},
"CompressedStreamTools": {
"fqn": "net.minecraft.nbt.CompressedStreamTools",
"category": "SERVICE",
"service": "NBTIO",
"imports": 24,
"methods": {
"readCompressed(InputStream)": "Read compressed NBT from stream. Returns INbt.",
"writeCompressed(NBTTagCompound, OutputStream)": "Write compressed NBT to stream."
},
"reason": "Static NBT I/O. Already abstracted in PlatformService.readCompressedNBT/writeCompressedNBT.",
"notes": "Already has Static-Transform patterns defined."
},
"CommandBase": {
"fqn": "net.minecraft.command.CommandBase",
"category": "SERVICE",
"service": "PlatformService",
"imports": 14,
"methods": {
"parseDouble(String)": "Parse double from command arg.",
"parseInt(String)": "Parse int from command arg.",
"getListOfStringsMatchingLastWord(String[], String...)": "Tab completion helper."
},
"reason": "Static command utilities. Commands stay in mc1710 but some utility methods are useful in core.",
"notes": "1.13+ Brigadier replaces the entire command system. These utilities become irrelevant."
},
"Loader": {
"fqn": "cpw.mods.fml.common.Loader",
"category": "SERVICE",
"service": "PlatformService",
"imports": 11,
"methods": {
"isModLoaded(String modid)": "Check if a mod is loaded."
},
"reason": "Static mod loader query. 1.13+: ModList.get().isLoaded().",
"notes": "Single useful method. Add to PlatformService.isModLoaded()."
},
"I18n": {
"fqn": "net.minecraft.client.resources.I18n",
"category": "SERVICE",
"service": "PlatformService",
"imports": 9,
"methods": {
"format(String key, Object... args)": "Client-side translation with formatting."
},
"reason": "Client-side I18n. Overlaps with StatCollector for server-side. Both map to PlatformService.translate().",
"notes": "Client-specific. May stay in mc1710 client code."
},
"DimensionManager": {
"fqn": "net.minecraftforge.common.DimensionManager",
"category": "SERVICE",
"service": "PlatformService",
"imports": 2,
"methods": {
"getWorld(int dimensionId)": "Get world by dimension ID. Returns IWorld.",
"getIDs()": "Get all registered dimension IDs."
},
"reason": "Forge dimension management. Used in core for world access.",
"notes": "1.16+ completely reworks dimensions (ResourceKey<Level>)."
},
"PlayerSelector": {
"fqn": "net.minecraft.command.PlayerSelector",
"category": "SERVICE",
"service": "PlatformService",
"imports": 2,
"methods": {
"matchPlayers(ICommandSender, String)": "Parse @p/@a/@r selectors. Returns List<IPlayer>."
},
"reason": "Command player selector parsing. 1.13+ Brigadier handles this natively.",
"notes": "Single static method."
},
"NBTUtil": {
"fqn": "net.minecraft.nbt.NBTUtil",
"category": "SERVICE",
"service": "NBTIO",
"imports": 1,
"methods": {
"func_152459_a(NBTTagCompound, GameProfile)": "Write GameProfile to NBT.",
"func_152458_a(NBTTagCompound)": "Read GameProfile from NBT."
},
"reason": "NBT utility for GameProfile serialization.",
"notes": "Very low usage. Consider suppressing."
},
"__comment_transform": "=== TRANSFORM: Regex-based Static-Transform replacement (10 types) ===",
"Side": {
"fqn": "cpw.mods.fml.relauncher.Side",
"category": "TRANSFORM",
"imports": 405,
"patterns": [
["@SideOnly\\s*\\(\\s*Side\\.CLIENT\\s*\\)", "@ClientOnly"],
["@SideOnly\\s*\\(\\s*Side\\.SERVER\\s*\\)", "@ServerOnly"],
["Side\\.CLIENT", "PlatformSide.CLIENT"],
["Side\\.SERVER", "PlatformSide.SERVER"]
],
"notes": "Highest-impact TRANSFORM. 800 imports (Side+SideOnly). Annotation swap eliminates both."
},
"SideOnly": {
"fqn": "cpw.mods.fml.relauncher.SideOnly",
"category": "TRANSFORM",
"imports": 395,
"patterns": [],
"notes": "Handled by Side patterns above. Import gets nuked, annotation text gets swapped."
},
"MathHelper": {
"fqn": "net.minecraft.util.MathHelper",
"category": "TRANSFORM",
"imports": 98,
"patterns": [
["MathHelper\\.clamp_int\\s*\\(", "ValueUtil.clampInt("],
["MathHelper\\.clamp_float\\s*\\(", "ValueUtil.clampFloat("],
["MathHelper\\.clamp_double\\s*\\(", "ValueUtil.clampDouble("],
["MathHelper\\.sqrt_double\\s*\\(", "Math.sqrt("],
["MathHelper\\.sqrt_float\\s*\\(", "(float)Math.sqrt("],
["MathHelper\\.floor_double\\s*\\(", "ValueUtil.floorDouble("],
["MathHelper\\.floor_float\\s*\\(", "ValueUtil.floorFloat("],
["MathHelper\\.sin\\s*\\(", "(float)Math.sin("],
["MathHelper\\.cos\\s*\\(", "(float)Math.cos("],
["MathHelper\\.abs\\s*\\(", "Math.abs("],
["MathHelper\\.ceiling_double_int\\s*\\(", "ValueUtil.ceilToInt("],
["MathHelper\\.ceiling_float_int\\s*\\(", "ValueUtil.ceilToInt("]
],
"notes": "Pure static math. All methods have Java stdlib or ValueUtil equivalents."
},
"EnumChatFormatting": {
"fqn": "net.minecraft.util.EnumChatFormatting",
"category": "TRANSFORM",
"imports": 19,
"patterns": [
["EnumChatFormatting\\.", "TextFormatting."]
],
"notes": "Color/formatting constants. Create a TextFormatting enum in core with same constants (RED, BLUE, BOLD, RESET, etc.). 1.16+: ChatFormatting."
},
"EnumFacing": {
"fqn": "net.minecraft.util.EnumFacing",
"category": "TRANSFORM",
"imports": 3,
"patterns": [
["EnumFacing\\.", "Direction."]
],
"notes": "Direction enum (UP/DOWN/NORTH/SOUTH/EAST/WEST). Create Direction enum in core. 1.13+: already Direction."
},
"SharedMonsterAttributes": {
"fqn": "net.minecraft.entity.SharedMonsterAttributes",
"category": "TRANSFORM",
"imports": 10,
"patterns": [
["SharedMonsterAttributes\\.maxHealth", "Attributes.MAX_HEALTH"],
["SharedMonsterAttributes\\.movementSpeed", "Attributes.MOVEMENT_SPEED"],
["SharedMonsterAttributes\\.attackDamage", "Attributes.ATTACK_DAMAGE"],
["SharedMonsterAttributes\\.followRange", "Attributes.FOLLOW_RANGE"],
["SharedMonsterAttributes\\.knockbackResistance", "Attributes.KNOCKBACK_RESISTANCE"]
],
"notes": "Attribute constants. Create Attributes class in core. These are IAttributeInstance references."
},
"Constants": {
"fqn": "net.minecraftforge.common.util.Constants",
"category": "TRANSFORM",
"imports": 9,
"patterns": [
["Constants\\.NBT\\.TAG_COMPOUND", "NbtConstants.TAG_COMPOUND"],
["Constants\\.NBT\\.TAG_STRING", "NbtConstants.TAG_STRING"],
["Constants\\.NBT\\.TAG_LIST", "NbtConstants.TAG_LIST"],
["Constants\\.NBT\\.TAG_INT", "NbtConstants.TAG_INT"],
["Constants\\.NBT\\.TAG_DOUBLE", "NbtConstants.TAG_DOUBLE"],
["Constants\\.NBT\\.TAG_BYTE", "NbtConstants.TAG_BYTE"],
["Constants\\.NBT\\.TAG_FLOAT", "NbtConstants.TAG_FLOAT"],
["Constants\\.NBT\\.TAG_LONG", "NbtConstants.TAG_LONG"],
["Constants\\.NBT\\.TAG_SHORT", "NbtConstants.TAG_SHORT"],
["Constants\\.NBT\\.TAG_INT_ARRAY", "NbtConstants.TAG_INT_ARRAY"],
["Constants\\.NBT\\.TAG_BYTE_ARRAY", "NbtConstants.TAG_BYTE_ARRAY"]
],
"notes": "Forge NBT type ID constants. Create NbtConstants class in core with same int values."
},
"CommandException": {
"fqn": "net.minecraft.command.CommandException",
"category": "TRANSFORM",
"imports": 31,
"patterns": [
["net\\.minecraft\\.command\\.CommandException", "noppes.npcs.api.CommandException"],
["new CommandException\\(", "new CommandException("]
],
"notes": "Exception class. Create a plain Java CommandException in core. Same constructor signature (String message, Object... args). No MC dependency needed."
},
"ChunkCoordinates": {
"fqn": "net.minecraft.util.ChunkCoordinates",
"category": "TRANSFORM",
"imports": 4,
"patterns": [
["new ChunkCoordinates\\(([^,]+),\\s*([^,]+),\\s*([^)]+)\\)", "new BlockPos($1, $2, $3)"],
["\\.posX\\b", ".getX()"],
["\\.posY\\b", ".getY()"],
["\\.posZ\\b", ".getZ()"]
],
"notes": "1.7.10 position class. Maps to IPos (already exists in platform-api) or inline x/y/z."
},
"EnumAction": {
"fqn": "net.minecraft.item.EnumAction",
"category": "TRANSFORM",
"imports": 11,
"patterns": [
["EnumAction\\.", "ItemAction."]
],
"notes": "Item use action enum (NONE, EAT, DRINK, BLOCK, BOW). Create ItemAction enum in core."
},
"__comment_suppress": "=== SUPPRESS: Never abstract. Code using these stays in mc1710. (352 types) ===",
"__suppress_legend": {
"reasons": {
"client": "Client-only GUI, rendering, models, textures, audio, shaders, fonts",
"networking": "Packets, buffers, network handlers, protocol classes",
"entity-hierarchy": "Entity subclasses that extend MC Entity directly. Entity code stays in mc1710",
"entity-ai": "AI tasks, pathfinding, navigation, data watchers",
"event-system": "Forge/FML event classes, SubscribeEvent, EventBus, event handlers",
"registration": "GameRegistry, EntityRegistry, NetworkRegistry — platform-specific registration",
"recipe-system": "CraftingManager, IRecipe, ShapedRecipes — NEVER touch (AGENTS.md rule)",
"nbt-subtype": "Individual NBT tag types (NBTTagString, NBTTagInt, etc.) — handled through INbt getters/setters",
"block-internal": "Block subclasses, block materials, block states — block system stays in mc1710",
"item-internal": "Item subclasses (ItemSword, ItemBow, etc.) — item classes stay in mc1710",
"forge-internal": "Forge/FML lifecycle, mod loading, annotations, proxies, reflection helpers",
"inventory": "Inventory implementations, container internals — GUI/inventory stays in mc1710",
"world-internal": "Chunks, biomes, world providers, dimension internals",
"debug": "Profiler, crash reports, debug utilities",
"scoreboard-internal": "Score, criteria — too niche for core abstraction"
}
},
"GuiButton": { "fqn": "net.minecraft.client.gui.GuiButton", "category": "SUPPRESS", "imports": 218, "reason": "client" },
"Minecraft": { "fqn": "net.minecraft.client.Minecraft", "category": "SUPPRESS", "imports": 180, "reason": "client" },
"ModelRenderer": { "fqn": "net.minecraft.client.model.ModelRenderer", "category": "SUPPRESS", "imports": 122, "reason": "client" },
"ModelBase": { "fqn": "net.minecraft.client.model.ModelBase", "category": "SUPPRESS", "imports": 102, "reason": "client" },
"Block": { "fqn": "net.minecraft.block.Block", "category": "SUPPRESS", "imports": 84, "reason": "block-internal", "notes": "IBlock exists in scripting API for block access through IWorld. Raw MC Block class stays in mc1710." },
"TileEntity": { "fqn": "net.minecraft.tileentity.TileEntity", "category": "SUPPRESS", "imports": 78, "reason": "block-internal", "notes": "ITileEntity exists in scripting API. Raw MC TileEntity stays in mc1710." },
"GuiScreen": { "fqn": "net.minecraft.client.gui.GuiScreen", "category": "SUPPRESS", "imports": 70, "reason": "client" },
"Tessellator": { "fqn": "net.minecraft.client.renderer.Tessellator", "category": "SUPPRESS", "imports": 46, "reason": "client" },
"Slot": { "fqn": "net.minecraft.inventory.Slot", "category": "SUPPRESS", "imports": 42, "reason": "inventory", "notes": "ISlot exists in scripting API for different purpose." },
"RenderHelper": { "fqn": "net.minecraft.client.renderer.RenderHelper", "category": "SUPPRESS", "imports": 40, "reason": "client" },
"OpenGlHelper": { "fqn": "net.minecraft.client.renderer.OpenGlHelper", "category": "SUPPRESS", "imports": 39, "reason": "client" },
"TextureManager": { "fqn": "net.minecraft.client.renderer.texture.TextureManager", "category": "SUPPRESS", "imports": 38, "reason": "client" },
"FontRenderer": { "fqn": "net.minecraft.client.gui.FontRenderer", "category": "SUPPRESS", "imports": 38, "reason": "client" },
"NBTTagString": { "fqn": "net.minecraft.nbt.NBTTagString", "category": "SUPPRESS", "imports": 35, "reason": "nbt-subtype", "notes": "Handled via INbt.setString/getString." },
"IIcon": { "fqn": "net.minecraft.util.IIcon", "category": "SUPPRESS", "imports": 35, "reason": "client" },
"RenderBlocks": { "fqn": "net.minecraft.client.renderer.RenderBlocks", "category": "SUPPRESS", "imports": 35, "reason": "client" },
"RenderManager": { "fqn": "net.minecraft.client.renderer.entity.RenderManager", "category": "SUPPRESS", "imports": 33, "reason": "client" },
"ModelBox": { "fqn": "net.minecraft.client.model.ModelBox", "category": "SUPPRESS", "imports": 32, "reason": "client" },
"IInventory": { "fqn": "net.minecraft.inventory.IInventory", "category": "SUPPRESS", "imports": 30, "reason": "inventory", "notes": "1.12+ uses IItemHandler capability. Complex to abstract." },
"IBlockAccess": { "fqn": "net.minecraft.world.IBlockAccess", "category": "SUPPRESS", "imports": 30, "reason": "client", "notes": "World read-only interface for rendering." },
"RenderingRegistry": { "fqn": "cpw.mods.fml.client.registry.RenderingRegistry", "category": "SUPPRESS", "imports": 28, "reason": "client" },
"EntityAIBase": { "fqn": "net.minecraft.entity.ai.EntityAIBase", "category": "SUPPRESS", "imports": 27, "reason": "entity-ai" },
"ScaledResolution": { "fqn": "net.minecraft.client.gui.ScaledResolution", "category": "SUPPRESS", "imports": 26, "reason": "client" },
"IIconRegister": { "fqn": "net.minecraft.client.renderer.texture.IIconRegister", "category": "SUPPRESS", "imports": 26, "reason": "client" },
"Container": { "fqn": "net.minecraft.inventory.Container", "category": "SUPPRESS", "imports": 25, "reason": "inventory", "notes": "IContainer exists in scripting API for different purpose." },
"CreativeTabs": { "fqn": "net.minecraft.creativetab.CreativeTabs", "category": "SUPPRESS", "imports": 25, "reason": "registration" },
"Material": { "fqn": "net.minecraft.block.material.Material", "category": "SUPPRESS", "imports": 24, "reason": "block-internal" },
"GuiYesNo": { "fqn": "net.minecraft.client.gui.GuiYesNo", "category": "SUPPRESS", "imports": 22, "reason": "client" },
"Gui": { "fqn": "net.minecraft.client.gui.Gui", "category": "SUPPRESS", "imports": 22, "reason": "client" },
"GuiYesNoCallback": { "fqn": "net.minecraft.client.gui.GuiYesNoCallback", "category": "SUPPRESS", "imports": 21, "reason": "client" },
"ItemBlock": { "fqn": "net.minecraft.item.ItemBlock", "category": "SUPPRESS", "imports": 19, "reason": "item-internal", "notes": "IItemBlock exists in scripting API." },
"Cancelable": { "fqn": "cpw.mods.fml.common.eventhandler.Cancelable", "category": "SUPPRESS", "imports": 19, "reason": "event-system" },
"GameRegistry": { "fqn": "cpw.mods.fml.common.registry.GameRegistry", "category": "SUPPRESS", "imports": 18, "reason": "registration" },
"Event": { "fqn": "cpw.mods.fml.common.eventhandler.Event", "category": "SUPPRESS", "imports": 17, "reason": "event-system" },
"BlockContainer": { "fqn": "net.minecraft.block.BlockContainer", "category": "SUPPRESS", "imports": 16, "reason": "block-internal" },
"ModelBiped": { "fqn": "net.minecraft.client.model.ModelBiped", "category": "SUPPRESS", "imports": 16, "reason": "client" },
"SubscribeEvent": { "fqn": "cpw.mods.fml.common.eventhandler.SubscribeEvent", "category": "SUPPRESS", "imports": 15, "reason": "event-system" },
"EntityItem": { "fqn": "net.minecraft.entity.item.EntityItem", "category": "SUPPRESS", "imports": 15, "reason": "entity-hierarchy", "notes": "IEntityItem exists in platform-api for scripting. Raw MC class stays in mc1710." },
"ItemArmor": { "fqn": "net.minecraft.item.ItemArmor", "category": "SUPPRESS", "imports": 13, "reason": "item-internal", "notes": "IItemArmor exists in platform-api for scripting. Raw MC class stays in mc1710." },
"IItemRenderer": { "fqn": "net.minecraftforge.client.IItemRenderer", "category": "SUPPRESS", "imports": 11, "reason": "client" },
"IResource": { "fqn": "net.minecraft.client.resources.IResource", "category": "SUPPRESS", "imports": 11, "reason": "client" },
"BlockPos": { "fqn": "net.minecraft.util.math.BlockPos", "category": "SUPPRESS", "imports": 10, "reason": "block-internal", "notes": "IPos exists in platform-api. BlockPos is 1.8+ only (not native to 1.7.10)." },
"Render": { "fqn": "net.minecraft.client.renderer.entity.Render", "category": "SUPPRESS", "imports": 8, "reason": "client" },
"ItemRenderer": { "fqn": "net.minecraft.client.renderer.ItemRenderer", "category": "SUPPRESS", "imports": 8, "reason": "client" },
"MinecraftForge": { "fqn": "net.minecraftforge.common.MinecraftForge", "category": "SUPPRESS", "imports": 8, "reason": "event-system" },
"RenderItem": { "fqn": "net.minecraft.client.renderer.entity.RenderItem", "category": "SUPPRESS", "imports": 8, "reason": "client" },
"EntityMob": { "fqn": "net.minecraft.entity.monster.EntityMob", "category": "SUPPRESS", "imports": 7, "reason": "entity-hierarchy", "notes": "IMonster exists in platform-api." },
"EntityCreature": { "fqn": "net.minecraft.entity.EntityCreature", "category": "SUPPRESS", "imports": 7, "reason": "entity-hierarchy", "notes": "IEntityLiving covers this in platform-api." },
"EntityAnimal": { "fqn": "net.minecraft.entity.passive.EntityAnimal", "category": "SUPPRESS", "imports": 7, "reason": "entity-hierarchy", "notes": "IAnimal exists in platform-api." },
"EntityTameable": { "fqn": "net.minecraft.entity.passive.EntityTameable", "category": "SUPPRESS", "imports": 7, "reason": "entity-hierarchy" },
"RendererLivingEntity": { "fqn": "net.minecraft.client.renderer.entity.RendererLivingEntity", "category": "SUPPRESS", "imports": 7, "reason": "client" },
"ISimpleBlockRenderingHandler": { "fqn": "cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler", "category": "SUPPRESS", "imports": 7, "reason": "client" },
"EntityClientPlayerMP": { "fqn": "net.minecraft.client.entity.EntityClientPlayerMP", "category": "SUPPRESS", "imports": 7, "reason": "client" },
"InventoryBasic": { "fqn": "net.minecraft.inventory.InventoryBasic", "category": "SUPPRESS", "imports": 7, "reason": "inventory" },
"Packet": { "fqn": "net.minecraft.network.Packet", "category": "SUPPRESS", "imports": 7, "reason": "networking" },
"Enchantment": { "fqn": "net.minecraft.enchantment.Enchantment", "category": "SUPPRESS", "imports": 2, "reason": "item-internal", "notes": "Too niche for core abstraction at 2 imports." },
"MerchantRecipeList": { "fqn": "net.minecraft.village.MerchantRecipeList", "category": "SUPPRESS", "imports": 6, "reason": "entity-hierarchy", "notes": "Trading system stays in mc1710." },
"MerchantRecipe": { "fqn": "net.minecraft.village.MerchantRecipe", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"ObfuscationReflectionHelper": { "fqn": "cpw.mods.fml.common.ObfuscationReflectionHelper", "category": "SUPPRESS", "imports": 6, "reason": "forge-internal" },
"EntityVillager": { "fqn": "net.minecraft.entity.passive.EntityVillager", "category": "SUPPRESS", "imports": 6, "reason": "entity-hierarchy", "notes": "IVillager exists in platform-api." },
"FakePlayer": { "fqn": "net.minecraftforge.common.util.FakePlayer", "category": "SUPPRESS", "imports": 6, "reason": "entity-hierarchy" },
"IEntitySelector": { "fqn": "net.minecraft.command.IEntitySelector", "category": "SUPPRESS", "imports": 6, "reason": "entity-ai" },
"S35PacketUpdateTileEntity": { "fqn": "net.minecraft.network.play.server.S35PacketUpdateTileEntity", "category": "SUPPRESS", "imports": 6, "reason": "networking" },
"ChatAllowedCharacters": { "fqn": "net.minecraft.util.ChatAllowedCharacters", "category": "SUPPRESS", "imports": 6, "reason": "client" },
"TickEvent": { "fqn": "cpw.mods.fml.common.gameevent.TickEvent", "category": "SUPPRESS", "imports": 6, "reason": "event-system" },
"TextureMap": { "fqn": "net.minecraft.client.renderer.texture.TextureMap", "category": "SUPPRESS", "imports": 6, "reason": "client" },
"NBTTagFloat": { "fqn": "net.minecraft.nbt.NBTTagFloat", "category": "SUPPRESS", "imports": 6, "reason": "nbt-subtype" },
"EntityXPOrb": { "fqn": "net.minecraft.entity.item.EntityXPOrb", "category": "SUPPRESS", "imports": 5, "reason": "entity-hierarchy" },
"Achievement": { "fqn": "net.minecraft.stats.Achievement", "category": "SUPPRESS", "imports": 5, "reason": "forge-internal", "notes": "Removed in 1.12+. Replaced by Advancements." },
"BlockColored": { "fqn": "net.minecraft.block.BlockColored", "category": "SUPPRESS", "imports": 5, "reason": "block-internal" },
"ModContainer": { "fqn": "cpw.mods.fml.common.ModContainer", "category": "SUPPRESS", "imports": 5, "reason": "forge-internal" },
"EntityThrowable": { "fqn": "net.minecraft.entity.projectile.EntityThrowable", "category": "SUPPRESS", "imports": 5, "reason": "entity-hierarchy", "notes": "IThrowable exists in platform-api." },
"EntityFX": { "fqn": "net.minecraft.client.particle.EntityFX", "category": "SUPPRESS", "imports": 5, "reason": "client" },
"FMLProxyPacket": { "fqn": "cpw.mods.fml.common.network.internal.FMLProxyPacket", "category": "SUPPRESS", "imports": 5, "reason": "networking" },
"PathEntity": { "fqn": "net.minecraft.pathfinding.PathEntity", "category": "SUPPRESS", "imports": 5, "reason": "entity-ai" },
"TileEntitySpecialRenderer": { "fqn": "net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer", "category": "SUPPRESS", "imports": 5, "reason": "client" },
"Blocks": { "fqn": "net.minecraft.init.Blocks", "category": "SUPPRESS", "imports": 47, "reason": "block-internal", "notes": "Block registry constants (Blocks.air, Blocks.stone). Access blocks through IBlock/IWorld." },
"Items": { "fqn": "net.minecraft.init.Items", "category": "SUPPRESS", "imports": 29, "reason": "item-internal", "notes": "Item registry constants. Access items through IItemStack." },
"EnumCreatureAttribute": { "fqn": "net.minecraft.entity.EnumCreatureAttribute", "category": "SUPPRESS", "imports": 4, "reason": "entity-hierarchy" },
"PlayerEvent": { "fqn": "cpw.mods.fml.common.gameevent.PlayerEvent", "category": "SUPPRESS", "imports": 4, "reason": "event-system" },
"IResourceManager": { "fqn": "net.minecraft.client.resources.IResourceManager", "category": "SUPPRESS", "imports": 4, "reason": "client" },
"PositionedSoundRecord": { "fqn": "net.minecraft.client.audio.PositionedSoundRecord", "category": "SUPPRESS", "imports": 4, "reason": "client" },
"NPCRendererHelper": { "fqn": "net.minecraft.client.renderer.entity.NPCRendererHelper", "category": "SUPPRESS", "imports": 4, "reason": "client" },
"GLAllocation": { "fqn": "net.minecraft.client.renderer.GLAllocation", "category": "SUPPRESS", "imports": 4, "reason": "client" },
"EntityDragon": { "fqn": "net.minecraft.entity.boss.EntityDragon", "category": "SUPPRESS", "imports": 4, "reason": "entity-hierarchy" },
"EntityArrow": { "fqn": "net.minecraft.entity.projectile.EntityArrow", "category": "SUPPRESS", "imports": 4, "reason": "entity-hierarchy", "notes": "IArrow exists in platform-api." },
"ItemTooltipEvent": { "fqn": "net.minecraftforge.event.entity.player.ItemTooltipEvent", "category": "SUPPRESS", "imports": 4, "reason": "event-system" },
"NetworkManager": { "fqn": "net.minecraft.network.NetworkManager", "category": "SUPPRESS", "imports": 4, "reason": "networking" },
"EntitySheep": { "fqn": "net.minecraft.entity.passive.EntitySheep", "category": "SUPPRESS", "imports": 4, "reason": "entity-hierarchy" },
"NBTTagByte": { "fqn": "net.minecraft.nbt.NBTTagByte", "category": "SUPPRESS", "imports": 4, "reason": "nbt-subtype" },
"ItemBow": { "fqn": "net.minecraft.item.ItemBow", "category": "SUPPRESS", "imports": 4, "reason": "item-internal" },
"NBTTagIntArray": { "fqn": "net.minecraft.nbt.NBTTagIntArray", "category": "SUPPRESS", "imports": 4, "reason": "nbt-subtype" },
"WorldEvent": { "fqn": "net.minecraftforge.event.world.WorldEvent", "category": "SUPPRESS", "imports": 4, "reason": "event-system" },
"PacketBuffer": { "fqn": "net.minecraft.network.PacketBuffer", "category": "SUPPRESS", "imports": 4, "reason": "networking" },
"EntityAITarget": { "fqn": "net.minecraft.entity.ai.EntityAITarget", "category": "SUPPRESS", "imports": 4, "reason": "entity-ai" },
"GuiContainer": { "fqn": "net.minecraft.client.gui.inventory.GuiContainer", "category": "SUPPRESS", "imports": 4, "reason": "client" },
"InventoryCrafting": { "fqn": "net.minecraft.inventory.InventoryCrafting", "category": "SUPPRESS", "imports": 4, "reason": "recipe-system" },
"AttributeModifier": { "fqn": "net.minecraft.entity.ai.attributes.AttributeModifier", "category": "SUPPRESS", "imports": 3, "reason": "entity-hierarchy" },
"SoundHandler": { "fqn": "net.minecraft.client.audio.SoundHandler", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"RenderBiped": { "fqn": "net.minecraft.client.renderer.entity.RenderBiped", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"ItemEditableBook": { "fqn": "net.minecraft.item.ItemEditableBook", "category": "SUPPRESS", "imports": 3, "reason": "item-internal" },
"ItemWritableBook": { "fqn": "net.minecraft.item.ItemWritableBook", "category": "SUPPRESS", "imports": 3, "reason": "item-internal" },
"ForgeHooksClient": { "fqn": "net.minecraftforge.client.ForgeHooksClient", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"MinecraftForgeClient": { "fqn": "net.minecraftforge.client.MinecraftForgeClient", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"ForgeChunkManager": { "fqn": "net.minecraftforge.common.ForgeChunkManager", "category": "SUPPRESS", "imports": 3, "reason": "world-internal" },
"BiomeGenBase": { "fqn": "net.minecraft.world.biome.BiomeGenBase", "category": "SUPPRESS", "imports": 3, "reason": "world-internal" },
"LivingEvent": { "fqn": "net.minecraftforge.event.entity.living.LivingEvent", "category": "SUPPRESS", "imports": 3, "reason": "event-system" },
"EntityInteractEvent": { "fqn": "net.minecraftforge.event.entity.player.EntityInteractEvent", "category": "SUPPRESS", "imports": 3, "reason": "event-system" },
"PlayerInteractEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerInteractEvent", "category": "SUPPRESS", "imports": 3, "reason": "event-system" },
"SimpleReloadableResourceManager": { "fqn": "net.minecraft.client.resources.SimpleReloadableResourceManager", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"KeyBinding": { "fqn": "net.minecraft.client.settings.KeyBinding", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"ICrafting": { "fqn": "net.minecraft.inventory.ICrafting", "category": "SUPPRESS", "imports": 3, "reason": "inventory" },
"ItemFood": { "fqn": "net.minecraft.item.ItemFood", "category": "SUPPRESS", "imports": 3, "reason": "item-internal" },
"PathNavigate": { "fqn": "net.minecraft.pathfinding.PathNavigate", "category": "SUPPRESS", "imports": 3, "reason": "entity-ai" },
"PathPoint": { "fqn": "net.minecraft.pathfinding.PathPoint", "category": "SUPPRESS", "imports": 3, "reason": "entity-ai" },
"AbstractClientPlayer": { "fqn": "net.minecraft.client.entity.AbstractClientPlayer", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"GuiTextField": { "fqn": "net.minecraft.client.gui.GuiTextField", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"InventoryPlayer": { "fqn": "net.minecraft.entity.player.InventoryPlayer", "category": "SUPPRESS", "imports": 3, "reason": "inventory" },
"EntityRenderer": { "fqn": "net.minecraft.client.renderer.EntityRenderer", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"RenderPlayer": { "fqn": "net.minecraft.client.renderer.entity.RenderPlayer", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"BlockIce": { "fqn": "net.minecraft.block.BlockIce", "category": "SUPPRESS", "imports": 3, "reason": "block-internal" },
"BlockLeavesBase": { "fqn": "net.minecraft.block.BlockLeavesBase", "category": "SUPPRESS", "imports": 3, "reason": "block-internal" },
"BlockVine": { "fqn": "net.minecraft.block.BlockVine", "category": "SUPPRESS", "imports": 3, "reason": "block-internal" },
"EntityHorse": { "fqn": "net.minecraft.entity.passive.EntityHorse", "category": "SUPPRESS", "imports": 3, "reason": "entity-hierarchy" },
"RenderWorldLastEvent": { "fqn": "net.minecraftforge.client.event.RenderWorldLastEvent", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"IEntityAdditionalSpawnData": { "fqn": "cpw.mods.fml.common.registry.IEntityAdditionalSpawnData", "category": "SUPPRESS", "imports": 3, "reason": "entity-hierarchy" },
"FMLNetworkEvent": { "fqn": "cpw.mods.fml.common.network.FMLNetworkEvent", "category": "SUPPRESS", "imports": 3, "reason": "networking" },
"ArmorMaterial": { "fqn": "net.minecraft.item.ItemArmor.ArmorMaterial", "category": "SUPPRESS", "imports": 3, "reason": "item-internal" },
"EntityEvent": { "fqn": "net.minecraftforge.event.entity.EntityEvent", "category": "SUPPRESS", "imports": 3, "reason": "event-system" },
"EntityDamageSource": { "fqn": "net.minecraft.util.EntityDamageSource", "category": "SUPPRESS", "imports": 3, "reason": "entity-hierarchy", "notes": "Subclass of DamageSource. IDamageSource covers the base." },
"ChunkCoordIntPair": { "fqn": "net.minecraft.world.ChunkCoordIntPair", "category": "SUPPRESS", "imports": 3, "reason": "world-internal" },
"EventPriority": { "fqn": "cpw.mods.fml.common.eventhandler.EventPriority", "category": "SUPPRESS", "imports": 3, "reason": "event-system" },
"GuiScreenEvent": { "fqn": "net.minecraftforge.client.event.GuiScreenEvent", "category": "SUPPRESS", "imports": 3, "reason": "client" },
"IExtendedEntityProperties": { "fqn": "net.minecraftforge.common.IExtendedEntityProperties", "category": "SUPPRESS", "imports": 3, "reason": "entity-hierarchy" },
"ItemSword": { "fqn": "net.minecraft.item.ItemSword", "category": "SUPPRESS", "imports": 3, "reason": "item-internal" },
"NBTTagDouble": { "fqn": "net.minecraft.nbt.NBTTagDouble", "category": "SUPPRESS", "imports": 9, "reason": "nbt-subtype" },
"NBTTagInt": { "fqn": "net.minecraft.nbt.NBTTagInt", "category": "SUPPRESS", "imports": 8, "reason": "nbt-subtype" },
"IAttributeInstance": { "fqn": "net.minecraft.entity.ai.attributes.IAttributeInstance", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"BlockEvent": { "fqn": "net.minecraftforge.event.world.BlockEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"ResourcePackRepository": { "fqn": "net.minecraft.client.resources.ResourcePackRepository", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"Ticket": { "fqn": "net.minecraftforge.common.ForgeChunkManager.Ticket", "category": "SUPPRESS", "imports": 2, "reason": "world-internal" },
"ScoreObjective": { "fqn": "net.minecraft.scoreboard.ScoreObjective", "category": "SUPPRESS", "imports": 2, "reason": "scoreboard-internal", "notes": "IScoreboardObjective exists in platform-api." },
"ScorePlayerTeam": { "fqn": "net.minecraft.scoreboard.ScorePlayerTeam", "category": "SUPPRESS", "imports": 2, "reason": "scoreboard-internal", "notes": "IScoreboardTeam exists in platform-api." },
"Scoreboard": { "fqn": "net.minecraft.scoreboard.Scoreboard", "category": "SUPPRESS", "imports": 2, "reason": "scoreboard-internal", "notes": "IScoreboard exists in platform-api." },
"NetworkRegistry": { "fqn": "cpw.mods.fml.common.network.NetworkRegistry", "category": "SUPPRESS", "imports": 2, "reason": "registration" },
"EntityRegistry": { "fqn": "cpw.mods.fml.common.registry.EntityRegistry", "category": "SUPPRESS", "imports": 2, "reason": "registration" },
"IResourcePack": { "fqn": "net.minecraft.client.resources.IResourcePack", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"CraftingManager": { "fqn": "net.minecraft.item.crafting.CraftingManager", "category": "SUPPRESS", "imports": 2, "reason": "recipe-system" },
"GameSettings": { "fqn": "net.minecraft.client.settings.GameSettings", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"TextureUtil": { "fqn": "net.minecraft.client.renderer.texture.TextureUtil", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"EnumHelper": { "fqn": "net.minecraftforge.common.util.EnumHelper", "category": "SUPPRESS", "imports": 2, "reason": "forge-internal" },
"OreDictionary": { "fqn": "net.minecraftforge.oredict.OreDictionary", "category": "SUPPRESS", "imports": 2, "reason": "forge-internal" },
"EnchantmentHelper": { "fqn": "net.minecraft.enchantment.EnchantmentHelper", "category": "SUPPRESS", "imports": 2, "reason": "item-internal" },
"Chunk": { "fqn": "net.minecraft.world.chunk.Chunk", "category": "SUPPRESS", "imports": 2, "reason": "world-internal" },
"EntityJoinWorldEvent": { "fqn": "net.minecraftforge.event.entity.EntityJoinWorldEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"ItemTossEvent": { "fqn": "net.minecraftforge.event.entity.item.ItemTossEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"PlayerUseItemEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerUseItemEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"LivingAttackEvent": { "fqn": "net.minecraftforge.event.entity.living.LivingAttackEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"LivingHurtEvent": { "fqn": "net.minecraftforge.event.entity.living.LivingHurtEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"IRangedAttackMob": { "fqn": "net.minecraft.entity.IRangedAttackMob", "category": "SUPPRESS", "imports": 2, "reason": "entity-ai" },
"NPCResourceHelper": { "fqn": "net.minecraft.client.resources.NPCResourceHelper", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"ActiveRenderInfo": { "fqn": "net.minecraft.client.renderer.ActiveRenderInfo", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"IMerchant": { "fqn": "net.minecraft.entity.IMerchant", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"BiomeDictionary": { "fqn": "net.minecraftforge.common.BiomeDictionary", "category": "SUPPRESS", "imports": 2, "reason": "world-internal" },
"InventoryCraftResult": { "fqn": "net.minecraft.inventory.InventoryCraftResult", "category": "SUPPRESS", "imports": 2, "reason": "recipe-system" },
"WeightedRandom": { "fqn": "net.minecraft.util.WeightedRandom", "category": "SUPPRESS", "imports": 2, "reason": "forge-internal" },
"EntityGhast": { "fqn": "net.minecraft.entity.monster.EntityGhast", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"S12PacketEntityVelocity": { "fqn": "net.minecraft.network.play.server.S12PacketEntityVelocity", "category": "SUPPRESS", "imports": 2, "reason": "networking" },
"EntityPlayerSP": { "fqn": "net.minecraft.client.entity.EntityPlayerSP", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"ItemRenderType": { "fqn": "net.minecraftforge.client.IItemRenderer.ItemRenderType", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"EnumCreatureType": { "fqn": "net.minecraft.entity.EnumCreatureType", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"ChunkDataEvent": { "fqn": "net.minecraftforge.event.world.ChunkDataEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"ChunkEvent": { "fqn": "net.minecraftforge.event.world.ChunkEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"ChunkWatchEvent": { "fqn": "net.minecraftforge.event.world.ChunkWatchEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"ContainerPlayer": { "fqn": "net.minecraft.inventory.ContainerPlayer", "category": "SUPPRESS", "imports": 2, "reason": "inventory" },
"LivingDeathEvent": { "fqn": "net.minecraftforge.event.entity.living.LivingDeathEvent", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"Phase": { "fqn": "cpw.mods.fml.common.gameevent.TickEvent.Phase", "category": "SUPPRESS", "imports": 2, "reason": "event-system" },
"EntityMoveHelper": { "fqn": "net.minecraft.entity.ai.EntityMoveHelper", "category": "SUPPRESS", "imports": 2, "reason": "entity-ai" },
"EnumSkyBlock": { "fqn": "net.minecraft.world.EnumSkyBlock", "category": "SUPPRESS", "imports": 2, "reason": "world-internal" },
"AbstractResourcePack": { "fqn": "net.minecraft.client.resources.AbstractResourcePack", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"FallbackResourceManager": { "fqn": "net.minecraft.client.resources.FallbackResourceManager", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"RenderLivingEvent": { "fqn": "net.minecraftforge.client.event.RenderLivingEvent", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"RenderPlayerEvent": { "fqn": "net.minecraftforge.client.event.RenderPlayerEvent", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"EntityPortalFX": { "fqn": "net.minecraft.client.particle.EntityPortalFX", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"SkinManager": { "fqn": "net.minecraft.client.resources.SkinManager", "category": "SUPPRESS", "imports": 2, "reason": "client" },
"EntityEnderman": { "fqn": "net.minecraft.entity.monster.EntityEnderman", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"EntityCreeper": { "fqn": "net.minecraft.entity.monster.EntityCreeper", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"IMob": { "fqn": "net.minecraft.entity.monster.IMob", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"INpc": { "fqn": "net.minecraft.entity.INpc", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy" },
"EntityFishHook": { "fqn": "net.minecraft.entity.projectile.EntityFishHook", "category": "SUPPRESS", "imports": 2, "reason": "entity-hierarchy", "notes": "IFishHook exists in platform-api." },
"NBTPrimitive": { "fqn": "net.minecraft.nbt.NBTBase.NBTPrimitive", "category": "SUPPRESS", "imports": 2, "reason": "nbt-subtype" },
"__comment_suppress_1import": "=== SUPPRESS: 1-import types (all stay in mc1710) ===",
"FMLEventChannel": { "fqn": "cpw.mods.fml.common.network.FMLEventChannel", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"IBlockSource": { "fqn": "net.minecraft.dispenser.IBlockSource", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"CommandBlockLogic": { "fqn": "net.minecraft.command.server.CommandBlockLogic", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"IntHashMap": { "fqn": "net.minecraft.util.math.IntHashMap", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"WorldProvider": { "fqn": "net.minecraft.world.WorldProvider", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"BlockDispenser": { "fqn": "net.minecraft.block.BlockDispenser", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"FMLServerAboutToStartEvent": { "fqn": "cpw.mods.fml.common.event.FMLServerAboutToStartEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"StringUtils": { "fqn": "net.minecraft.util.StringUtils", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"MobSpawnerBaseLogic": { "fqn": "net.minecraft.tileentity.MobSpawnerBaseLogic", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"TileEntityCommandBlock": { "fqn": "net.minecraft.tileentity.TileEntityCommandBlock", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"TileEntityMobSpawner": { "fqn": "net.minecraft.tileentity.TileEntityMobSpawner", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"ForgeHooks": { "fqn": "net.minecraftforge.common.ForgeHooks", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"LivingFallEvent": { "fqn": "net.minecraftforge.event.entity.living.LivingFallEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlayerDropsEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerDropsEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"EntityEnderPearl": { "fqn": "net.minecraft.entity.item.EntityEnderPearl", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"ITickable": { "fqn": "net.minecraft.client.renderer.texture.ITickable", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"MouseEvent": { "fqn": "net.minecraftforge.client.event.MouseEvent", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ClientRegistry": { "fqn": "cpw.mods.fml.client.registry.ClientRegistry", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"IImageBuffer": { "fqn": "net.minecraft.client.renderer.IImageBuffer", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"Util": { "fqn": "net.minecraft.util.Util", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"ReflectionHelper": { "fqn": "cpw.mods.fml.relauncher.ReflectionHelper", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"SoundRegistry": { "fqn": "net.minecraft.client.audio.SoundRegistry", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"TexturedQuad": { "fqn": "net.minecraft.client.model.TexturedQuad", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"BossStatus": { "fqn": "net.minecraft.entity.boss.BossStatus", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ItemPotion": { "fqn": "net.minecraft.item.ItemPotion", "category": "SUPPRESS", "imports": 1, "reason": "item-internal" },
"PotionHelper": { "fqn": "net.minecraft.potion.PotionHelper", "category": "SUPPRESS", "imports": 1, "reason": "item-internal" },
"ModelBook": { "fqn": "net.minecraft.client.model.ModelBook", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"NPCEntityHelper": { "fqn": "net.minecraft.entity.NPCEntityHelper", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"FMLLaunchHandler": { "fqn": "cpw.mods.fml.relauncher.FMLLaunchHandler", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"StatList": { "fqn": "net.minecraft.stats.StatList", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"WorldSettings": { "fqn": "net.minecraft.world.WorldSettings", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"Score": { "fqn": "net.minecraft.scoreboard.Score", "category": "SUPPRESS", "imports": 1, "reason": "scoreboard-internal" },
"FMLClientHandler": { "fqn": "cpw.mods.fml.client.FMLClientHandler", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"PlayerNotFoundException": { "fqn": "net.minecraft.command.PlayerNotFoundException", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal", "notes": "Subclass of CommandException. Use core CommandException." },
"NetHandlerPlayServer": { "fqn": "net.minecraft.network.NetHandlerPlayServer", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"GuiAchievement": { "fqn": "net.minecraft.client.gui.achievement.GuiAchievement", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"NBTSizeTracker": { "fqn": "net.minecraft.nbt.NBTSizeTracker", "category": "SUPPRESS", "imports": 1, "reason": "nbt-subtype" },
"IGuiHandler": { "fqn": "cpw.mods.fml.common.network.IGuiHandler", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"BehaviorDefaultDispenseItem": { "fqn": "net.minecraft.dispenser.BehaviorDefaultDispenseItem", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"ToolMaterial": { "fqn": "net.minecraft.item.Item.ToolMaterial", "category": "SUPPRESS", "imports": 1, "reason": "item-internal" },
"ItemFlintAndSteel": { "fqn": "net.minecraft.item.ItemFlintAndSteel", "category": "SUPPRESS", "imports": 1, "reason": "item-internal" },
"Mod": { "fqn": "cpw.mods.fml.common.Mod", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"EventHandler": { "fqn": "cpw.mods.fml.common.Mod.EventHandler", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"SidedProxy": { "fqn": "cpw.mods.fml.common.SidedProxy", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"FMLLoadCompleteEvent": { "fqn": "cpw.mods.fml.common.event.FMLLoadCompleteEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"FMLServerStartedEvent": { "fqn": "cpw.mods.fml.common.event.FMLServerStartedEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"FMLServerStoppedEvent": { "fqn": "cpw.mods.fml.common.event.FMLServerStoppedEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"FMLInitializationEvent": { "fqn": "cpw.mods.fml.common.event.FMLInitializationEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"FMLPreInitializationEvent": { "fqn": "cpw.mods.fml.common.event.FMLPreInitializationEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"FMLServerStartingEvent": { "fqn": "cpw.mods.fml.common.event.FMLServerStartingEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"ForgeEventFactory": { "fqn": "net.minecraftforge.event.ForgeEventFactory", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"Teleporter": { "fqn": "net.minecraft.world.Teleporter", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"LoaderState": { "fqn": "cpw.mods.fml.common.LoaderState", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"EntityDamageSourceIndirect": { "fqn": "net.minecraft.util.EntityDamageSourceIndirect", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"Result": { "fqn": "cpw.mods.fml.common.eventhandler.Event.Result", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"ChunkPosition": { "fqn": "net.minecraft.world.ChunkPosition", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"AnvilRepairEvent": { "fqn": "net.minecraftforge.event.entity.player.AnvilRepairEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlayerDestroyItemEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerDestroyItemEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"EntityStruckByLightningEvent": { "fqn": "net.minecraftforge.event.entity.EntityStruckByLightningEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlaySoundAtEntityEvent": { "fqn": "net.minecraftforge.event.entity.PlaySoundAtEntityEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"AchievementEvent": { "fqn": "net.minecraftforge.event.entity.player.AchievementEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"ArrowLooseEvent": { "fqn": "net.minecraftforge.event.entity.player.ArrowLooseEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"ArrowNockEvent": { "fqn": "net.minecraftforge.event.entity.player.ArrowNockEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"BonemealEvent": { "fqn": "net.minecraftforge.event.entity.player.BonemealEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"FillBucketEvent": { "fqn": "net.minecraftforge.event.entity.player.FillBucketEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlayerOpenContainerEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerOpenContainerEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlayerPickupXpEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerPickupXpEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlayerSleepInBedEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerSleepInBedEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PlayerWakeUpEvent": { "fqn": "net.minecraftforge.event.entity.player.PlayerWakeUpEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"UseHoeEvent": { "fqn": "net.minecraftforge.event.entity.player.UseHoeEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"PopulateChunkEvent": { "fqn": "net.minecraftforge.event.terraingen.PopulateChunkEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"EntityAIDoorInteract": { "fqn": "net.minecraft.entity.ai.EntityAIDoorInteract", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"BlockDoor": { "fqn": "net.minecraft.block.BlockDoor", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"BlockFence": { "fqn": "net.minecraft.block.BlockFence", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"BlockFenceGate": { "fqn": "net.minecraft.block.BlockFenceGate", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"BlockRailBase": { "fqn": "net.minecraft.block.BlockRailBase", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"BlockWall": { "fqn": "net.minecraft.block.BlockWall", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"ChunkCache": { "fqn": "net.minecraft.world.ChunkCache", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"EntityAINearestAttackableTarget": { "fqn": "net.minecraft.entity.ai.EntityAINearestAttackableTarget", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"BlockBreakable": { "fqn": "net.minecraft.block.BlockBreakable", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"ForgeDirection": { "fqn": "net.minecraftforge.common.util.ForgeDirection", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"RenderGameOverlayEvent": { "fqn": "net.minecraftforge.client.event.RenderGameOverlayEvent", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"RenderHandEvent": { "fqn": "net.minecraftforge.client.event.RenderHandEvent", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"EntityFlameFX": { "fqn": "net.minecraft.client.particle.EntityFlameFX", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"EntitySmokeFX": { "fqn": "net.minecraft.client.particle.EntitySmokeFX", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"IReloadableResourceManager": { "fqn": "net.minecraft.client.resources.IReloadableResourceManager", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ReportedException": { "fqn": "net.minecraft.util.ReportedException", "category": "SUPPRESS", "imports": 1, "reason": "debug" },
"ClientCommandHandler": { "fqn": "net.minecraftforge.client.ClientCommandHandler", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"FolderResourcePack": { "fqn": "net.minecraft.client.resources.FolderResourcePack", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"IResourceManagerReloadListener": { "fqn": "net.minecraft.client.resources.IResourceManagerReloadListener", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"EntityChicken": { "fqn": "net.minecraft.entity.passive.EntityChicken", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"SimpleTexture": { "fqn": "net.minecraft.client.renderer.texture.SimpleTexture", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"InputEvent": { "fqn": "cpw.mods.fml.common.gameevent.InputEvent", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ClickEvent": { "fqn": "net.minecraft.event.ClickEvent", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ISound": { "fqn": "net.minecraft.client.audio.ISound", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ITickableSound": { "fqn": "net.minecraft.client.audio.ITickableSound", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"MovingSound": { "fqn": "net.minecraft.client.audio.MovingSound", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"EntityConstructing": { "fqn": "net.minecraftforge.event.entity.EntityEvent.EntityConstructing", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"C17PacketCustomPayload": { "fqn": "net.minecraft.network.play.client.C17PacketCustomPayload", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"ShaderGroup": { "fqn": "net.minecraft.client.shader.ShaderGroup", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"GuiOpenEvent": { "fqn": "net.minecraftforge.client.event.GuiOpenEvent", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"GuiConfirmOpenLink": { "fqn": "net.minecraft.client.gui.GuiConfirmOpenLink", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"DefaultResourcePack": { "fqn": "net.minecraft.client.resources.DefaultResourcePack", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ITextureObject": { "fqn": "net.minecraft.client.renderer.texture.ITextureObject", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"PositionTextureVertex": { "fqn": "net.minecraft.client.model.PositionTextureVertex", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ImageBufferDownload": { "fqn": "net.minecraft.client.renderer.ImageBufferDownload", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ItemRendererHelper": { "fqn": "net.minecraftforge.client.IItemRenderer.ItemRendererHelper", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"SlotCrafting": { "fqn": "net.minecraft.inventory.SlotCrafting", "category": "SUPPRESS", "imports": 1, "reason": "recipe-system" },
"S2FPacketSetSlot": { "fqn": "net.minecraft.network.play.server.S2FPacketSetSlot", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"LoadingCallback": { "fqn": "net.minecraftforge.common.ForgeChunkManager.LoadingCallback", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"EnumEnchantmentType": { "fqn": "net.minecraft.enchantment.EnumEnchantmentType", "category": "SUPPRESS", "imports": 1, "reason": "item-internal" },
"EntityAIAttackOnCollide": { "fqn": "net.minecraft.entity.ai.EntityAIAttackOnCollide", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"EntityAIHurtByTarget": { "fqn": "net.minecraft.entity.ai.EntityAIHurtByTarget", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"EntityAIOpenDoor": { "fqn": "net.minecraft.entity.ai.EntityAIOpenDoor", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"EntityAIRestrictSun": { "fqn": "net.minecraft.entity.ai.EntityAIRestrictSun", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"EntityAISwimming": { "fqn": "net.minecraft.entity.ai.EntityAISwimming", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"EntityAITaskEntry": { "fqn": "net.minecraft.entity.ai.EntityAITasks.EntityAITaskEntry", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"IBossDisplayData": { "fqn": "net.minecraft.entity.boss.IBossDisplayData", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"EntityZombie": { "fqn": "net.minecraft.entity.monster.EntityZombie", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"EntityBat": { "fqn": "net.minecraft.entity.passive.EntityBat", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"S0BPacketAnimation": { "fqn": "net.minecraft.network.play.server.S0BPacketAnimation", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"ServerChatEvent": { "fqn": "net.minecraftforge.event.ServerChatEvent", "category": "SUPPRESS", "imports": 1, "reason": "event-system" },
"S27PacketExplosion": { "fqn": "net.minecraft.network.play.server.S27PacketExplosion", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"MovingObjectType": { "fqn": "net.minecraft.util.MovingObjectPosition.MovingObjectType", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy", "notes": "Inner enum. Covered by IRayTraceResult.getType()." },
"EntitySkeleton": { "fqn": "net.minecraft.entity.monster.EntitySkeleton", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"S04PacketEntityEquipment": { "fqn": "net.minecraft.network.play.server.S04PacketEntityEquipment", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"ItemColored": { "fqn": "net.minecraft.item.ItemColored", "category": "SUPPRESS", "imports": 1, "reason": "item-internal" },
"FMLMessage": { "fqn": "cpw.mods.fml.common.network.internal.FMLMessage", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"EntityTrackerEntry": { "fqn": "net.minecraft.entity.EntityTrackerEntry", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"EnumDifficulty": { "fqn": "net.minecraft.world.EnumDifficulty", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"CommandHandler": { "fqn": "net.minecraft.command.CommandHandler", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"Vec3i": { "fqn": "net.minecraft.util.Vec3i", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"BlockAir": { "fqn": "net.minecraft.block.BlockAir", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"EntityLightningBolt": { "fqn": "net.minecraft.entity.effect.EntityLightningBolt", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"TileEntitySign": { "fqn": "net.minecraft.tileentity.TileEntitySign", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"BlockSnapshot": { "fqn": "net.minecraftforge.common.util.BlockSnapshot", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"StatBase": { "fqn": "net.minecraft.stats.StatBase", "category": "SUPPRESS", "imports": 1, "reason": "forge-internal" },
"IPlantable": { "fqn": "net.minecraftforge.common.IPlantable", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"IScoreObjectiveCriteria": { "fqn": "net.minecraft.scoreboard.IScoreObjectiveCriteria", "category": "SUPPRESS", "imports": 1, "reason": "scoreboard-internal" },
"NBTTagByteArray": { "fqn": "net.minecraft.nbt.NBTTagByteArray", "category": "SUPPRESS", "imports": 1, "reason": "nbt-subtype" },
"NBTTagLong": { "fqn": "net.minecraft.nbt.NBTTagLong", "category": "SUPPRESS", "imports": 1, "reason": "nbt-subtype" },
"NBTTagShort": { "fqn": "net.minecraft.nbt.NBTTagShort", "category": "SUPPRESS", "imports": 1, "reason": "nbt-subtype" },
"IWorldAccess": { "fqn": "net.minecraft.world.IWorldAccess", "category": "SUPPRESS", "imports": 1, "reason": "world-internal" },
"GuiInventory": { "fqn": "net.minecraft.client.gui.inventory.GuiInventory", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"C0DPacketCloseWindow": { "fqn": "net.minecraft.network.play.client.C0DPacketCloseWindow", "category": "SUPPRESS", "imports": 1, "reason": "networking" },
"PathFinder": { "fqn": "net.minecraft.pathfinding.PathFinder", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"MapColor": { "fqn": "net.minecraft.block.material.MapColor", "category": "SUPPRESS", "imports": 1, "reason": "block-internal" },
"RenderGlobal": { "fqn": "net.minecraft.client.renderer.RenderGlobal", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"IRecipe": { "fqn": "net.minecraft.item.crafting.IRecipe", "category": "SUPPRESS", "imports": 1, "reason": "recipe-system" },
"GuiSlot": { "fqn": "net.minecraft.client.gui.GuiSlot", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"RenderLiving": { "fqn": "net.minecraft.client.renderer.entity.RenderLiving", "category": "SUPPRESS", "imports": 1, "reason": "client" },
"ShapedRecipes": { "fqn": "net.minecraft.item.crafting.ShapedRecipes", "category": "SUPPRESS", "imports": 1, "reason": "recipe-system" },
"DataWatcher": { "fqn": "net.minecraft.entity.DataWatcher", "category": "SUPPRESS", "imports": 1, "reason": "entity-hierarchy" },
"EntityAITasks": { "fqn": "net.minecraft.entity.ai.EntityAITasks", "category": "SUPPRESS", "imports": 1, "reason": "entity-ai" },
"EventBus": { "fqn": "cpw.mods.fml.common.eventhandler.EventBus", "category": "SUPPRESS", "imports": 1, "reason": "event-system" }
}
}
File diff suppressed because it is too large. Load diff
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""PA Batch Writer — Write multiple Java source files from a single JSON manifest.
Usage:
python tools/migration/pa_batch.py <manifest.json> [options]
Reads a JSON manifest containing raw Java source strings and writes them to disk.
Auto-derives file paths from package declarations and public type names when
no explicit path is provided.
See PA_IMPLEMENTOR_DESIGN.md for full design rationale and manifest format.
"""
import argparse
import json
import os
import re
import sys
RE_PACKAGE = re.compile(r"^\s*package\s+([\w.]+)\s*;", re.MULTILINE)
RE_PUBLIC_TYPE = re.compile(
r"public\s+(?:abstract\s+|final\s+|strictfp\s+)*"
r"(?:class|interface|enum|@interface)\s+(\w+)",
re.MULTILINE,
)
def detect_project_root(start: str) -> str:
"""Walk up from *start* looking for a directory that contains settings.gradle."""
cur = os.path.abspath(start)
for _ in range(20):
if os.path.isfile(os.path.join(cur, "settings.gradle")):
return cur
parent = os.path.dirname(cur)
if parent == cur:
break
cur = parent
return os.path.abspath(start)
def derive_path(content: str, root: str) -> tuple:
"""Derive the output file path from *content*'s package + public type name.
Returns ``(path, error_message)``. On success *error_message* is ``None``.
"""
pkg_match = RE_PACKAGE.search(content)
if not pkg_match:
return None, "no package declaration found"
type_match = RE_PUBLIC_TYPE.search(content)
if not type_match:
return None, "no public class/interface/enum/@interface found"
pkg = pkg_match.group(1).replace(".", os.sep)
name = type_match.group(1)
return os.path.join(root, pkg, f"{name}.java"), None
def validate_package_vs_path(content: str, path: str) -> str | None:
"""Return a warning string if *path* doesn't match the package in *content*."""
pkg_match = RE_PACKAGE.search(content)
if not pkg_match:
return None
expected_pkg_dir = pkg_match.group(1).replace(".", os.sep)
norm = path.replace("\\", "/")
expected = expected_pkg_dir.replace("\\", "/")
if expected not in norm:
return f'package "{pkg_match.group(1)}" does not match path "{path}"'
return None
def process_manifest(
manifest_path: str, project_root: str, force: bool, dry_run: bool, json_output: bool
):
"""Read *manifest_path* and write (or preview) all files."""
with open(manifest_path, "r", encoding="utf-8") as fh:
manifest = json.load(fh)
global_root = manifest.get("root", "")
entries = manifest.get("files", [])
created: list[str] = []
skipped: list[str] = []
errors: list[dict] = []
warnings: list[str] = []
for idx, entry in enumerate(entries):
content = entry.get("content")
if content is None:
errors.append({"index": idx, "error": 'missing "content" field'})
continue
explicit_path = entry.get("path")
entry_root = entry.get("root", global_root)
if explicit_path:
rel_path = explicit_path
warn = validate_package_vs_path(content, rel_path)
if warn:
warnings.append(f"entry {idx}: {warn}")
else:
if not entry_root:
errors.append(
{
"index": idx,
"error": "cannot derive path — no root and no explicit path",
}
)
continue
rel_path, err = derive_path(content, entry_root)
if err:
errors.append({"index": idx, "error": f"cannot derive path — {err}"})
continue
abs_path = os.path.join(project_root, rel_path)
if os.path.isfile(abs_path) and not force:
skipped.append(rel_path)
continue
if not dry_run:
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
with open(abs_path, "w", encoding="utf-8", newline="\n") as out:
out.write(content)
created.append(rel_path)
if json_output:
result = {
"total": len(entries),
"created": created,
"skipped": skipped,
"errors": errors,
"warnings": warnings,
"dry_run": dry_run,
}
print(json.dumps(result, indent=2))
else:
mode = " (DRY RUN)" if dry_run else ""
print(f"\nPA Batch Writer — {len(entries)} files processed{mode}")
print(f" CREATED: {len(created)} files")
print(f" SKIPPED: {len(skipped)} files (already exist)")
print(f" ERRORS: {len(errors)}")
if warnings:
print(f" WARNINGS: {len(warnings)}")
if created:
verb = "Would create" if dry_run else "Created"
print(f"\n{verb}:")
for p in created:
print(f" {p}")
if skipped:
print("\nSkipped (already exist):")
for p in skipped:
print(f" {p}")
if errors:
print("\nErrors:")
for e in errors:
print(f" entry {e['index']}: {e['error']}")
if warnings:
print("\nWarnings:")
for w in warnings:
print(f" {w}")
return len(errors) == 0
def main():
parser = argparse.ArgumentParser(
description="PA Batch Writer — write multiple Java source files from a JSON manifest.",
)
parser.add_argument("manifest", help="Path to JSON manifest file")
parser.add_argument("--force", action="store_true", help="Overwrite existing files")
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be created without writing",
)
parser.add_argument(
"--json", action="store_true", help="Machine-readable JSON output"
)
parser.add_argument(
"--project-root",
default=None,
help="Project root override (auto-detected by default)",
)
args = parser.parse_args()
if not os.path.isfile(args.manifest):
print(f"ERROR: manifest not found: {args.manifest}", file=sys.stderr)
sys.exit(1)
if args.project_root:
project_root = os.path.abspath(args.project_root)
else:
project_root = detect_project_root(os.path.dirname(os.path.abspath(__file__)))
try:
ok = process_manifest(
args.manifest, project_root, args.force, args.dry_run, args.json
)
except json.JSONDecodeError as exc:
print(f"ERROR: invalid JSON in manifest — {exc}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
+411
View File
@@ -0,0 +1,411 @@
EXISTING:
NBTTagCompound -> INbt
EntityPlayer -> IPlayer
EntityPlayerMP -> IPlayer
Entity -> IEntity
ItemStack -> IItemStack
EntityLivingBase -> IEntityLivingBase
World -> IWorld
NBTTagList -> INbtList
DamageSource -> IDamageSource
WorldServer -> IWorld
EntityLiving -> IEntityLiving
INTERFACE:
ResourceLocation -> IResourceLocation
Block -> IBlock
TileEntity -> ITileEntity
AxisAlignedBB -> IBoundingBox
ICommandSender -> ICommandSender
Vec3 -> IVector3
Slot -> ISlot
RenderBlocks -> IRenderBlocks
IInventory -> IIInventory
IBlockAccess -> IIBlockAccess
Container -> IContainer
ChatComponentTranslation -> ITextComponent
ChatComponentText -> ITextComponent
EntityItem -> IEntityItem
MovingObjectPosition -> IRayTraceResult
ItemArmor -> IItemArmor
NBTBase -> INbtBase
BlockPos -> IBlockPos
IChatComponent -> ITextComponent
Configuration -> IConfiguration
RenderItem -> IRenderItem
EntityMob -> IEntityMob
EntityCreature -> IEntityLiving
Potion -> IPotionType
Property -> IProperty
PotionEffect -> IPotionEffect
MerchantRecipeList -> IMerchantRecipeList
ModContainer -> IModContainer
InventoryCrafting -> IInventoryCrafting
AttributeModifier -> IAttributeModifier
Explosion -> IExplosion
ChatStyle -> ITextStyle
SoundHandler -> ISoundHandler
IAttributeInstance -> IIAttributeInstance
BlockEvent -> IBlockEvent
ResourcePackRepository -> IResourcePackRepository
Ticket -> ITicket
Enchantment -> IEnchantment
ScoreObjective -> IScoreObjective
ScorePlayerTeam -> IScorePlayerTeam
Scoreboard -> IScoreboard
FMLEventChannel -> IFMLEventChannel
IBlockSource -> IIBlockSource
CommandBlockLogic -> ICommandBlockLogic
IntHashMap -> IIntHashMap
MerchantRecipe -> IMerchantRecipe
SERVICE:
StatCollector -> PlatformService
Item -> PlatformService
RenderingRegistry -> PlatformService
MinecraftServer -> PlatformService
IIconRegister -> PlatformService
EntityList -> PlatformService
FMLCommonHandler -> PlatformService
CompressedStreamTools -> NBTIO
Gui -> PlatformService
CommandBase -> PlatformService
Loader -> PlatformService
IItemRenderer -> PlatformService
IResource -> PlatformService
I18n -> PlatformService
FMLLog -> PlatformService
ItemRenderer -> PlatformService
ObfuscationReflectionHelper -> PlatformService
EntityVillager -> PlatformService
FakePlayer -> PlatformService
IEntitySelector -> PlatformService
S35PacketUpdateTileEntity -> PlatformService
ChatAllowedCharacters -> PlatformService
EntityXPOrb -> PlatformService
Achievement -> PlatformService
BlockColored -> PlatformService
EnumCreatureAttribute -> PlatformService
PlayerEvent -> PlatformService
IResourceManager -> PlatformService
PositionedSoundRecord -> PlatformService
NPCRendererHelper -> PlatformService
GLAllocation -> PlatformService
RenderBiped -> PlatformService
ItemEditableBook -> PlatformService
ItemWritableBook -> PlatformService
ForgeHooksClient -> PlatformService
MinecraftForgeClient -> PlatformService
ForgeChunkManager -> PlatformService
BiomeGenBase -> PlatformService
LivingEvent -> PlatformService
EntityInteractEvent -> PlatformService
PlayerInteractEvent -> PlatformService
SimpleReloadableResourceManager -> PlatformService
KeyBinding -> PlatformService
ICrafting -> PlatformService
ItemFood -> PlatformService
PlayerSelector -> PlatformService
DimensionManager -> PlatformService
GameSettings -> PlatformService
TextureUtil -> PlatformService
EnumHelper -> PlatformService
OreDictionary -> PlatformService
EnchantmentHelper -> PlatformService
Chunk -> PlatformService
EntityJoinWorldEvent -> PlatformService
ItemTossEvent -> PlatformService
PlayerUseItemEvent -> PlatformService
LivingAttackEvent -> PlatformService
LivingHurtEvent -> PlatformService
IRangedAttackMob -> PlatformService
NPCResourceHelper -> PlatformService
ActiveRenderInfo -> PlatformService
IMerchant -> PlatformService
BiomeDictionary -> PlatformService
InventoryCraftResult -> PlatformService
WeightedRandom -> PlatformService
WorldProvider -> PlatformService
BlockDispenser -> PlatformService
FMLServerAboutToStartEvent -> PlatformService
NBTUtil -> PlatformService
StringUtils -> PlatformService
MobSpawnerBaseLogic -> PlatformService
TileEntityCommandBlock -> PlatformService
TileEntityMobSpawner -> PlatformService
ForgeHooks -> PlatformService
LivingFallEvent -> PlatformService
PlayerDropsEvent -> PlatformService
EntityEnderPearl -> PlatformService
ITickable -> PlatformService
MouseEvent -> PlatformService
ClientRegistry -> PlatformService
IImageBuffer -> PlatformService
Util -> PlatformService
ReflectionHelper -> PlatformService
SoundRegistry -> PlatformService
TexturedQuad -> PlatformService
BossStatus -> PlatformService
ItemPotion -> PlatformService
PotionHelper -> PlatformService
ModelBook -> PlatformService
NPCEntityHelper -> PlatformService
FMLLaunchHandler -> PlatformService
StatList -> PlatformService
WorldSettings -> PlatformService
Score -> PlatformService
FMLClientHandler -> PlatformService
TRANSFORM:
Side -> (2 patterns)
SideOnly -> (0 patterns)
MathHelper -> (7 patterns)
EnumChatFormatting -> (0 patterns)
EnumFacing -> (0 patterns)
SUPPRESS:
GuiButton
Minecraft
ModelRenderer
ModelBase
GuiScreen
Tessellator
RenderHelper
OpenGlHelper
TextureManager
FontRenderer
IIcon
RenderManager
EntityAIBase
ScaledResolution
CreativeTabs
GameRegistry
Event
ModelBiped
SubscribeEvent
Render
MinecraftForge
EntityClientPlayerMP
InventoryBasic
Packet
FMLProxyPacket
PathEntity
TileEntitySpecialRenderer
PacketBuffer
EntityAITarget
GuiContainer
RenderPlayer
PathNavigate
PathPoint
AbstractClientPlayer
GuiTextField
InventoryPlayer
EntityRenderer
NetworkRegistry
EntityRegistry
IResourcePack
CraftingManager
FMLInitializationEvent
FMLPreInitializationEvent
FMLServerStartingEvent
ForgeEventFactory
PathFinder
MapColor
RenderGlobal
IRecipe
GuiSlot
RenderLiving
ShapedRecipes
DataWatcher
EntityAITasks
EventBus
AUTO:
Blocks
NBTTagString
ModelBox
CommandException
Items
Material
GuiYesNo
GuiYesNoCallback
ItemBlock
Cancelable
BlockContainer
EnumAction
SharedMonsterAttributes
Constants
NBTTagDouble
NBTTagInt
EntityAnimal
EntityTameable
RendererLivingEntity
ISimpleBlockRenderingHandler
TickEvent
TextureMap
NBTTagFloat
EntityThrowable
EntityFX
EntityDragon
ChunkCoordinates
WorldEvent
EntityArrow
ItemTooltipEvent
NetworkManager
EntitySheep
NBTTagByte
ItemBow
NBTTagIntArray
BlockIce
BlockLeavesBase
BlockVine
EntityHorse
RenderWorldLastEvent
IEntityAdditionalSpawnData
FMLNetworkEvent
ArmorMaterial
EntityEvent
EntityDamageSource
ChunkCoordIntPair
EventPriority
GuiScreenEvent
IExtendedEntityProperties
ItemSword
EntityGhast
S12PacketEntityVelocity
EntityPlayerSP
ItemRenderType
EnumCreatureType
ChunkDataEvent
ChunkEvent
ChunkWatchEvent
ContainerPlayer
LivingDeathEvent
Phase
EntityMoveHelper
EnumSkyBlock
AbstractResourcePack
FallbackResourceManager
RenderLivingEvent
RenderPlayerEvent
EntityPortalFX
SkinManager
EntityEnderman
EntityCreeper
IMob
INpc
EntityFishHook
NBTPrimitive
PlayerNotFoundException
NetHandlerPlayServer
GuiAchievement
NBTSizeTracker
IGuiHandler
BehaviorDefaultDispenseItem
ToolMaterial
ItemFlintAndSteel
Mod
EventHandler
SidedProxy
FMLLoadCompleteEvent
FMLServerStartedEvent
FMLServerStoppedEvent
Teleporter
LoaderState
EntityDamageSourceIndirect
Result
ChunkPosition
AnvilRepairEvent
PlayerDestroyItemEvent
EntityStruckByLightningEvent
PlaySoundAtEntityEvent
AchievementEvent
ArrowLooseEvent
ArrowNockEvent
BonemealEvent
FillBucketEvent
PlayerOpenContainerEvent
PlayerPickupXpEvent
PlayerSleepInBedEvent
PlayerWakeUpEvent
UseHoeEvent
PlayerEvent
Action
PopulateChunkEvent
EntityAIDoorInteract
BlockDoor
BlockFence
BlockFenceGate
BlockRailBase
BlockWall
ChunkCache
EntityAINearestAttackableTarget
BlockBreakable
ForgeDirection
RenderGameOverlayEvent
RenderHandEvent
EntityFlameFX
EntitySmokeFX
IReloadableResourceManager
ReportedException
ClientCommandHandler
FolderResourcePack
IResourceManagerReloadListener
EntityChicken
SimpleTexture
InputEvent
ClickEvent
Action
ISound
ITickableSound
MovingSound
EntityConstructing
C17PacketCustomPayload
ShaderGroup
GuiOpenEvent
GuiConfirmOpenLink
DefaultResourcePack
ITextureObject
PositionTextureVertex
ImageBufferDownload
ItemRendererHelper
SlotCrafting
S2FPacketSetSlot
LoadingCallback
Type
EnumEnchantmentType
EntityAIAttackOnCollide
EntityAIHurtByTarget
EntityAIOpenDoor
EntityAIRestrictSun
EntityAISwimming
EntityAITaskEntry
IBossDisplayData
EntityZombie
EntityBat
S0BPacketAnimation
ServerChatEvent
S27PacketExplosion
MovingObjectType
EntitySkeleton
S04PacketEntityEquipment
ItemColored
FMLMessage
EntityTrackerEntry
EnumDifficulty
CommandHandler
Type
Vec3i
BlockAir
EntityLightningBolt
TileEntitySign
BlockSnapshot
StatBase
IPlantable
IScoreObjectiveCriteria
NBTTagByteArray
NBTTagLong
NBTTagShort
IWorldAccess
GuiInventory
C0DPacketCloseWindow
+518
View File
@@ -0,0 +1,518 @@
#!/usr/bin/env python3
"""PA Batch Editor — Surgical multi-file editing from a single JSON manifest.
Usage:
python tools/migration/pa_editor.py <manifest.json> [options]
Companion to pa_batch.py (which creates files). This tool EDITS existing files
with atomic transaction semantics: all edits succeed or ALL rollback.
Operations: insert (before/after line), delete (N lines), replace (pattern→content).
Targeting: line numbers OR regex patterns (agent chooses per-edit).
See AGENTS.md in tools/migration/ for full manifest format and usage patterns.
"""
import argparse
import copy
import difflib
import json
import os
import re
import shutil
import sys
from typing import Any
def detect_project_root(start: str) -> str:
"""Walk up from *start* looking for settings.gradle."""
cur = os.path.abspath(start)
for _ in range(20):
if os.path.isfile(os.path.join(cur, "settings.gradle")):
return cur
parent = os.path.dirname(cur)
if parent == cur:
break
cur = parent
return os.path.abspath(start)
# ---------------------------------------------------------------------------
# Edit validation
# ---------------------------------------------------------------------------
def _resolve_line(
lines: list[str], target: dict[str, Any], filepath: str
) -> tuple[int | None, str | None]:
"""Resolve a target spec to a 0-based line index.
Target formats:
{"line": 10} → line 10 (1-based → index 9)
{"pattern": "import.*"} → first line matching regex
{"pattern": "import.*", "occurrence": 2} → 2nd match
Returns (index, error_msg). On success error_msg is None.
"""
if "line" in target:
ln = target["line"]
if not isinstance(ln, int) or ln < 1:
return None, f"invalid line number: {ln}"
if ln > len(lines):
return None, f"line {ln} out of bounds (file has {len(lines)} lines)"
return ln - 1, None
if "pattern" in target:
pat = target["pattern"]
try:
regex = re.compile(pat)
except re.error as exc:
return None, f"invalid regex '{pat}': {exc}"
occurrence = target.get("occurrence", 1)
if not isinstance(occurrence, int) or occurrence < 1:
return None, f"invalid occurrence: {occurrence}"
count = 0
for i, line in enumerate(lines):
if regex.search(line):
count += 1
if count == occurrence:
return i, None
return None, f"pattern '{pat}' not found in {filepath}" + (
f" (occurrence {occurrence})" if occurrence > 1 else ""
)
return None, "target must specify 'line' or 'pattern'"
def _validate_edit(edit: dict[str, Any], idx: int) -> str | None:
"""Validate a single edit spec. Returns error message or None."""
op = edit.get("op")
if op not in ("insert", "delete", "replace"):
return f"edit {idx}: invalid op '{op}' (must be insert/delete/replace)"
if op == "insert":
if "content" not in edit:
return f"edit {idx}: insert requires 'content'"
if "target" not in edit:
return f"edit {idx}: insert requires 'target'"
pos = edit.get("position", "after")
if pos not in ("before", "after"):
return f"edit {idx}: position must be 'before' or 'after', got '{pos}'"
elif op == "delete":
if "target" not in edit:
return f"edit {idx}: delete requires 'target'"
count = edit.get("count", 1)
if not isinstance(count, int) or count < 1:
return f"edit {idx}: delete count must be a positive integer"
elif op == "replace":
if "content" not in edit:
return f"edit {idx}: replace requires 'content'"
if "target" not in edit:
return f"edit {idx}: replace requires 'target'"
count = edit.get("count", 1)
if not isinstance(count, int) or count < 1:
return f"edit {idx}: replace count must be a positive integer"
return None
# ---------------------------------------------------------------------------
# Edit application (on in-memory line list)
# ---------------------------------------------------------------------------
def _apply_edit(
lines: list[str], edit: dict[str, Any], idx: int, filepath: str
) -> tuple[list[str], str | None]:
"""Apply a single edit to *lines* (in-memory). Returns (new_lines, error_msg).
Edits are applied to a copy. On error, returns (original_lines, error_msg).
"""
op = edit["op"]
target = edit["target"]
line_idx, err = _resolve_line(lines, target, filepath)
if err or line_idx is None:
return lines, f"edit {idx}: {err}"
result = list(lines)
if op == "insert":
content = edit["content"]
new_lines = _content_to_lines(content)
pos = edit.get("position", "after")
insert_at = line_idx if pos == "before" else line_idx + 1
for i, nl in enumerate(new_lines):
result.insert(insert_at + i, nl)
elif op == "delete":
count = edit.get("count", 1)
end = line_idx + count
if end > len(result):
return (
lines,
f"edit {idx}: delete would go past end of file (line {line_idx + 1} + {count} > {len(result)})",
)
del result[line_idx:end]
elif op == "replace":
count = edit.get("count", 1)
end = line_idx + count
if end > len(result):
return (
lines,
f"edit {idx}: replace range past end of file (line {line_idx + 1} + {count} > {len(result)})",
)
new_lines = _content_to_lines(edit["content"])
result[line_idx:end] = new_lines
return result, None
def _content_to_lines(content: str | list[str]) -> list[str]:
"""Normalize content (string or list of strings) to list of lines with newlines."""
if isinstance(content, list):
return [ln if ln.endswith("\n") else ln + "\n" for ln in content]
# String: split into lines, preserve trailing newline
raw_lines = content.split("\n")
# If content ends with \n, split produces an empty trailing element — drop it
if raw_lines and raw_lines[-1] == "":
raw_lines = raw_lines[:-1]
return [ln + "\n" for ln in raw_lines]
# ---------------------------------------------------------------------------
# Diff generation
# ---------------------------------------------------------------------------
def _generate_diff(
old_lines: list[str], new_lines: list[str], filepath: str
) -> list[str]:
"""Generate unified diff between old and new content."""
return list(
difflib.unified_diff(
old_lines,
new_lines,
fromfile=f"a/{filepath}",
tofile=f"b/{filepath}",
lineterm="\n",
)
)
# ---------------------------------------------------------------------------
# Core processing
# ---------------------------------------------------------------------------
def process_manifest(
manifest_path: str,
project_root: str,
force: bool,
dry_run: bool,
backup: bool,
json_output: bool,
) -> bool:
"""Read *manifest_path*, validate ALL edits, then apply atomically (or preview)."""
with open(manifest_path, "r", encoding="utf-8") as fh:
manifest = json.load(fh)
file_edits: list[dict[str, Any]] = manifest.get("edits", [])
if not file_edits:
_output(json_output, dry_run, [], [], [], [])
return True
# ======================================================================
# PHASE 1: Structural validation (no file I/O)
# ======================================================================
errors: list[dict[str, Any]] = []
for fi, fe in enumerate(file_edits):
path = fe.get("path")
if not path:
errors.append({"file_index": fi, "error": "missing 'path'"})
continue
ops = fe.get("ops", [])
if not ops:
errors.append({"file_index": fi, "path": path, "error": "no ops specified"})
continue
for oi, op_spec in enumerate(ops):
err = _validate_edit(op_spec, oi)
if err:
errors.append({"file_index": fi, "path": path, "error": err})
if errors:
_output(json_output, dry_run, [], [], errors, [])
return False
# ======================================================================
# PHASE 2: Read all files, resolve all targets, compute results
# ======================================================================
file_states: list[
dict[str, Any]
] = [] # {path, abs_path, original, modified, diffs, edit_count}
warnings: list[str] = []
for fi, fe in enumerate(file_edits):
rel_path = fe["path"]
abs_path = os.path.join(project_root, rel_path)
if not os.path.isfile(abs_path):
errors.append(
{"file_index": fi, "path": rel_path, "error": "file not found"}
)
continue
try:
with open(abs_path, "r", encoding="utf-8") as f:
original = f.readlines()
except Exception as exc:
errors.append(
{"file_index": fi, "path": rel_path, "error": f"read error: {exc}"}
)
continue
# Apply edits sequentially (order matters — line numbers shift)
current = list(original)
ops = fe["ops"]
edit_count = 0
file_ok = True
for oi, op_spec in enumerate(ops):
current, err = _apply_edit(current, op_spec, oi, rel_path)
if err:
errors.append({"file_index": fi, "path": rel_path, "error": err})
file_ok = False
break
edit_count += 1
if not file_ok:
continue
if current == original:
warnings.append(f"{rel_path}: edits produced no changes")
diffs = _generate_diff(original, current, rel_path)
file_states.append(
{
"path": rel_path,
"abs_path": abs_path,
"original": original,
"modified": current,
"diffs": diffs,
"edit_count": edit_count,
}
)
# If ANY file had errors, abort ALL
if errors:
_output(json_output, dry_run, [], [], errors, warnings)
return False
# ======================================================================
# PHASE 3: Apply (or dry-run)
# ======================================================================
modified_paths: list[str] = []
backup_paths: list[str] = []
if not dry_run:
# Create backups first (if requested), so rollback is possible
if backup:
for fs in file_states:
bak_path = fs["abs_path"] + ".bak"
try:
shutil.copy2(fs["abs_path"], bak_path)
backup_paths.append(bak_path)
except Exception as exc:
errors.append(
{"path": fs["path"], "error": f"backup failed: {exc}"}
)
if errors:
# Clean up any backups we already made
for bp in backup_paths:
try:
os.remove(bp)
except OSError:
pass
_output(json_output, dry_run, [], [], errors, warnings)
return False
written: list[str] = []
write_error = False
for fs in file_states:
try:
with open(fs["abs_path"], "w", encoding="utf-8", newline="") as out:
out.writelines(fs["modified"])
written.append(fs["abs_path"])
modified_paths.append(fs["path"])
except Exception as exc:
errors.append({"path": fs["path"], "error": f"write failed: {exc}"})
write_error = True
break
# ROLLBACK on write failure
if write_error:
for abs_p in written:
# Find the original content for this path
for fs in file_states:
if fs["abs_path"] == abs_p:
try:
with open(abs_p, "w", encoding="utf-8", newline="") as out:
out.writelines(fs["original"])
except Exception:
pass # best-effort rollback
break
# Also remove backups since we rolled back
for bp in backup_paths:
try:
os.remove(bp)
except OSError:
pass
_output(json_output, dry_run, [], [], errors, warnings)
return False
else:
modified_paths = [fs["path"] for fs in file_states]
_output(json_output, dry_run, file_states, modified_paths, errors, warnings)
return True
# ---------------------------------------------------------------------------
# Output formatting
# ---------------------------------------------------------------------------
def _output(
json_mode: bool,
dry_run: bool,
file_states: list[dict[str, Any]],
modified: list[str],
errors: list[dict[str, Any]],
warnings: list[str],
):
"""Print results in human-readable or JSON format."""
total_edits = sum(fs.get("edit_count", 0) for fs in file_states)
if json_mode:
result: dict[str, Any] = {
"dry_run": dry_run,
"files_modified": len(modified),
"total_edits": total_edits,
"modified": modified,
"errors": errors,
"warnings": warnings,
}
if dry_run and file_states:
result["diffs"] = {fs["path"]: "".join(fs["diffs"]) for fs in file_states}
print(json.dumps(result, indent=2))
return
mode = " (DRY RUN)" if dry_run else ""
print(f"\nPA Batch Editor — {len(modified)} files, {total_edits} edits{mode}")
print(f" MODIFIED: {len(modified)} files")
print(f" ERRORS: {len(errors)}")
if warnings:
print(f" WARNINGS: {len(warnings)}")
if modified and not dry_run:
print("\nModified:")
for p in modified:
edit_count = 0
for fs in file_states:
if fs["path"] == p:
edit_count = fs["edit_count"]
break
print(f" {p} ({edit_count} edits)")
if dry_run and file_states:
print("\nDiffs:")
for fs in file_states:
if fs["diffs"]:
print(f"\n--- {fs['path']} ({fs['edit_count']} edits) ---")
for d in fs["diffs"]:
line = d.rstrip("\n")
print(line)
else:
print(f"\n--- {fs['path']} (no changes) ---")
if errors:
print("\nErrors:")
for e in errors:
path_info = e.get("path", f"index {e.get('file_index', '?')}")
print(f" {path_info}: {e['error']}")
if warnings:
print("\nWarnings:")
for w in warnings:
print(f" {w}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="PA Batch Editor — surgical multi-file editing from a JSON manifest.",
)
parser.add_argument("manifest", help="Path to JSON edit manifest file")
parser.add_argument(
"--force", action="store_true", help="Apply edits (required to write)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show diffs without writing (default if --force not given)",
)
parser.add_argument(
"--backup",
action="store_true",
help="Save .bak copies of original files before editing",
)
parser.add_argument(
"--json", action="store_true", help="Machine-readable JSON output"
)
parser.add_argument(
"--project-root",
default=None,
help="Project root override (auto-detected by default)",
)
args = parser.parse_args()
# Default to dry-run if --force not specified
if not args.force and not args.dry_run:
args.dry_run = True
if not os.path.isfile(args.manifest):
print(f"ERROR: manifest not found: {args.manifest}", file=sys.stderr)
sys.exit(1)
if args.project_root:
project_root = os.path.abspath(args.project_root)
else:
project_root = detect_project_root(os.path.dirname(os.path.abspath(__file__)))
try:
ok = process_manifest(
args.manifest,
project_root,
args.force,
args.dry_run,
args.backup,
args.json,
)
except json.JSONDecodeError as exc:
print(f"ERROR: invalid JSON in manifest — {exc}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
+791
View File
@@ -0,0 +1,791 @@
"""
PA Generation Rules — Single Source of Truth for Migration
==========================================================
This module defines ALL rules governing platform-abstraction (PA) generation.
Both analyze.py and scan_codebase.py import from here.
DESIGN PRINCIPLE: Every section is a flat data structure (dict, list, or scalar).
To add a new rule, just append an entry. No classes, no inheritance, no builders.
HOW TO USE:
from pa_rules import EXISTING_MAPPINGS # replaces hardcoded KNOWN_PA
from pa_rules import classify_type # get classification for any MC type
from pa_rules import get_pa_name # get semantic PA interface name
from pa_rules import get_service_target # get service routing for SERVICE types
from pa_rules import resolve_param_type # resolve MC type → PA type for params
HOW TO EXTEND:
- New PA mapping completed? → Add to EXISTING_MAPPINGS
- New type to classify? → Add to FORCE_INTERFACE, FORCE_SERVICE, FORCE_TRANSFORM, or SUPPRESS
- New semantic name? → Add to NAMING
- New service routing? → Add to SERVICE_ROUTING
- New obfuscated method name? → Add to OBFUSCATED_METHOD_NAMES
"""
import re
# ============================================================
# SECTION 1: EXISTING MAPPINGS
# Types already abstracted in platform-api.
# Used as the single source of truth for readiness scoring.
#
# Format: "net.minecraft.fully.qualified.Name": "PAInterfaceName"
#
# HOW TO UPDATE: When you complete a PA interface migration,
# add the MC FQN → PA name mapping here. Both analyze.py and
# scan_codebase.py will pick it up automatically.
# ============================================================
EXISTING_MAPPINGS = {
# --- NBT ---
"net.minecraft.nbt.NBTTagCompound": "INbt",
"net.minecraft.nbt.NBTTagList": "INbtList",
# --- Entity ---
"net.minecraft.entity.Entity": "IEntity",
"net.minecraft.entity.EntityLivingBase": "IEntityLivingBase",
"net.minecraft.entity.EntityLiving": "IEntityLiving",
"net.minecraft.entity.player.EntityPlayer": "IPlayer",
"net.minecraft.entity.player.EntityPlayerMP": "IPlayer",
# --- Item ---
"net.minecraft.item.ItemStack": "IItemStack",
# --- World ---
"net.minecraft.world.World": "IWorld",
"net.minecraft.world.WorldServer": "IWorld",
# --- Combat ---
"net.minecraft.util.DamageSource": "IDamageSource",
}
# ============================================================
# SECTION 2: TYPE CLASSIFICATION
# Controls how unabstracted MC types are handled during migration.
# Every MC type falls into exactly ONE category:
#
# EXISTING — Already has a PA interface (in EXISTING_MAPPINGS)
# INTERFACE — Should become a standalone PA interface (I*.java)
# SERVICE — Methods should be added to PlatformService or a domain service
# TRANSFORM — Handled by Static-Transform regex replacement
# SUPPRESS — Should NEVER be abstracted (client-only, debug, stays in mc1710)
# AUTO — Not explicitly classified; use heuristics (surface-miner data)
#
# HOW TO ADD: Put the MC simple class name (not FQN) in the right list.
# ============================================================
# --- Auto-classification thresholds (for types not in any list) ---
# Used by the classify command when a type isn't explicitly categorized.
# If more than this fraction of a type's used methods are static → SERVICE
STATIC_THRESHOLD = 0.6
# If a type has fewer than this many unique methods used → SERVICE (not worth an interface)
MIN_METHODS_FOR_INTERFACE = 3
# If a type is used in fewer than this many files → consider suppressing
MIN_FILES_FOR_INTERFACE = 5
# --- INTERFACE: Types that MUST become standalone PA interfaces ---
# These have rich instance-method surfaces and appear as parameters/return types.
# They represent concepts that exist across MC versions.
FORCE_INTERFACE = [
# Command/Chat system
"ICommandSender", # 5 instance methods, used for command execution context
"IChatComponent", # Chat message abstraction (→ ITextComponent)
"ChatComponentText", # Concrete text component (→ ITextComponent)
"ChatComponentTranslation", # Translatable text (→ ITextComponent)
"ChatStyle", # Text formatting (→ ITextStyle)
# Geometry / Math
"AxisAlignedBB", # Bounding boxes (→ IBoundingBox)
"Vec3", # 3D vector (→ IVector3)
"MovingObjectPosition", # Raytrace result (→ IRayTraceResult)
# Resources
"ResourceLocation", # Asset/registry identifiers (→ IResourceLocation)
# Effects
"Potion", # Potion type registry (→ IPotionType)
"PotionEffect", # Active potion effect instance (→ IPotionEffect)
# Enchantment
"Enchantment", # Enchantment type (→ IEnchantment)
# Auth
"GameProfile", # Mojang auth profile (→ IGameProfile)
# NBT extension (if needed beyond INbt)
"NBTBase", # Base NBT tag (→ INbtBase, if needed)
]
# --- SERVICE: Types whose methods → PlatformService or domain service ---
# These are static utility classes or singleton accessors.
# Java 7 can't have static methods in interfaces, so these MUST go to services.
FORCE_SERVICE = [
"CompressedStreamTools", # → NBTIO: readCompressed, writeCompressed, read
"PlayerSelector", # → PlatformService: matchPlayers (1 static method)
"MinecraftServer", # → PlatformService: getServer, isDedicatedServer, worldServerForDimension
"StringTranslate", # → PlatformService: translate
"FMLCommonHandler", # → PlatformService: getSide, getEffectiveSide
"ServerConfigurationManager", # → PlatformService: getPlayerList, sendPacketToAllPlayers
"JsonToNBT", # → PlatformService: parseNbt (1 static method)
"StatCollector", # → PlatformService: translateToLocal, translateToLocalFormatted
]
# --- TRANSFORM: Types handled by regex-based Static-Transform ---
# These are annotations, enums, or trivially-replaceable patterns.
# The actual regex patterns are in TRANSFORM_PATTERNS below.
FORCE_TRANSFORM = [
"Side", # @SideOnly(Side.CLIENT) → @ClientOnly
"SideOnly", # Annotation carrier (handled by Side patterns)
"MathHelper", # MathHelper.clamp_int → ValueUtil.clampInt (pure static math)
"EnumChatFormatting", # Color codes → string constants or ITextFormatting enum
"EnumFacing", # Direction enum (if used in core)
]
# --- SUPPRESS: Types that should NEVER be abstracted ---
# Client-only, networking, debug, entity AI internals, registration.
# These stay in mc1710 forever. Don't waste time classifying them.
SUPPRESS = [
# ---- Client-only (GUI, rendering) — never goes to core ----
"GuiButton",
"GuiScreen",
"GuiTextField",
"GuiContainer",
"GuiSlot",
"Minecraft",
"FontRenderer",
"Tessellator",
"RenderHelper",
"GL11",
"GL12",
"OpenGlHelper",
"RenderGlobal",
"TextureManager",
"IIcon",
"ScaledResolution",
"RenderManager",
"Render",
"RenderLiving",
"RenderPlayer",
"EntityRenderer",
"TileEntitySpecialRenderer",
"ModelBase",
"ModelRenderer",
"ModelBiped",
"ModelPlayer",
"EnumParticleTypes",
"WorldRenderer",
"AbstractClientPlayer",
"EntityClientPlayerMP",
"ResourcePack",
"IResourcePack",
# ---- Networking — stays in mc1710 platform layer ----
"Packet",
"PacketBuffer",
"ByteBuf",
"ByteBufInputStream",
"SimpleNetworkWrapper",
"IMessage",
"IMessageHandler",
"MessageContext",
"FMLProxyPacket",
# ---- Entity AI internals — entities extend MC directly ----
"EntityAIBase",
"EntityAITasks",
"PathNavigate",
"DataWatcher",
"EntityAITarget",
"EntityAIWatchClosest",
"PathFinder",
"NavigationPath",
"PathPoint",
"PathEntity",
# ---- Debug / Internal ----
"Profiler",
"CrashReport",
"CrashReportCategory",
# ---- Forge event system — stays in mc1710 ----
"Event",
"EventBus",
"SubscribeEvent",
"ForgeEventFactory",
"MinecraftForge",
"FMLEvent",
"FMLPreInitializationEvent",
"FMLInitializationEvent",
"FMLPostInitializationEvent",
"FMLServerStartingEvent",
"FMLServerStoppingEvent",
# ---- Registration — platform-specific forever ----
"GameRegistry",
"LanguageRegistry",
"EntityRegistry",
"NetworkRegistry",
"TickRegistry",
# ---- Mixin internals ----
"SpongePowered",
"Mixin",
"Shadow",
"Inject",
# ---- Recipe system (NEVER list in AGENTS.md) ----
"ShapedRecipes",
"ShapelessRecipes",
"CraftingManager",
"IRecipe",
# ---- Block/Tile internals that stay platform-side ----
# (IBlock/ITileEntity exist but wrap ScriptedBlock, not raw MC Block)
"CreativeTabs",
"MapColor",
# ---- Inventory internals ----
"InventoryBasic",
"InventoryPlayer",
]
# ============================================================
# SECTION 3: NAMING
# Semantic name overrides for generated PA interfaces.
# Maps MC simple class name → PA interface name.
#
# WHY: MC names are version-specific (ChatComponentText in 1.7,
# TextComponent in 1.16, Component in 1.19). PA names must be
# version-agnostic and stable across all versions.
#
# DEFAULT: If not listed here, the generated name is "I" + MCClassName.
#
# HOW TO ADD: When you decide on a semantic name for a type,
# add it here. The classify and generate-pa commands use this.
# ============================================================
NAMING = {
# ---- Chat / Text system ----
# 1.7: ChatComponentText, 1.16: StringTextComponent, 1.19: Component.literal()
"ChatComponentText": "ITextComponent",
"ChatComponentTranslation": "ITextComponent",
"ChatComponentScore": "ITextComponent",
"IChatComponent": "ITextComponent",
"ChatStyle": "ITextStyle",
"EnumChatFormatting": "ITextFormatting",
# ---- Server ----
# 1.7: MinecraftServer.getServer(), 1.13+: different access patterns
"MinecraftServer": "IServer",
"DedicatedServer": "IServer",
"IntegratedServer": "IServer",
"ServerConfigurationManager": "IPlayerManager",
# ---- Geometry ----
# 1.7: AxisAlignedBB, 1.14+: same name but different package
"AxisAlignedBB": "IBoundingBox",
"Vec3": "IVector3",
"ChunkCoordinates": "IPos", # IPos already exists in PA
"MovingObjectPosition": "IRayTraceResult",
# ---- Effects ----
# 1.7: Potion, 1.9+: different registry system but same concept
"Potion": "IPotionType",
"PotionEffect": "IPotionEffect",
# ---- Entity merges ----
# Multiple MC classes → single PA interface (wraps closest common ancestor)
"EntityPlayer": "IPlayer",
"EntityPlayerMP": "IPlayer",
"EntityLivingBase": "IEntityLivingBase",
"EntityLiving": "IEntityLiving",
"EntityCreature": "IEntityLiving",
# ---- Resources ----
# 1.7: ResourceLocation, all versions: same concept
"ResourceLocation": "IResourceLocation",
# ---- Auth ----
"GameProfile": "IGameProfile",
# ---- Enchantment ----
"Enchantment": "IEnchantment",
# ---- NBT base ----
"NBTBase": "INbtBase",
# ---- World ----
"WorldServer": "IWorld",
# ---- Command ----
"ICommandSender": "ICommandSender", # Keep same name (already good)
}
# ============================================================
# SECTION 4: ROUTING
# Where generated interfaces and service methods go.
#
# HOW TO UPDATE:
# - New sub-package needed? → Add a pattern to PACKAGE_ROUTING
# - New service target? → Add to SERVICE_ROUTING
# ============================================================
# Default output locations
DEFAULT_INTERFACE_PACKAGE = "noppes.npcs.api"
DEFAULT_INTERFACE_DIR = "platform-api/src/main/java/noppes/npcs/api/"
# Sub-package routing: regex on PA interface name → sub-package
# First match wins. If nothing matches → DEFAULT_INTERFACE_PACKAGE.
PACKAGE_ROUTING = [
(r"IPlayer|IEntity|ILiving|ICustomNpc", "noppes.npcs.api.entity"),
(r"IItem", "noppes.npcs.api.item"),
(r"IScoreboard", "noppes.npcs.api.scoreboard"),
(r"IOverlay", "noppes.npcs.api.overlay"),
(r"IJob", "noppes.npcs.api.jobs"),
(r"IRole", "noppes.npcs.api.roles"),
(r"IHandler|IPlayerData|IPlayerQuest", "noppes.npcs.api.handler"),
# Everything else → default package
]
# Service routing: MC simple name → service target
# Defines WHERE service methods go for FORCE_SERVICE types.
SERVICE_ROUTING = {
"CompressedStreamTools": {
"service": "NBTIO",
"file": "platform-api/src/main/java/noppes/npcs/platform/nbt/NBTIO.java",
"reason": "NBT I/O operations belong with NBT infrastructure",
},
"PlayerSelector": {
"service": "PlatformService",
"file": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"reason": "Single static method (matchPlayers), too small for own service",
},
"MinecraftServer": {
"service": "PlatformService",
"file": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"reason": "Server access is a core platform concern (getServer, isDedicatedServer)",
},
"StringTranslate": {
"service": "PlatformService",
"file": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"reason": "Translation is platform-specific (different I18n in each version)",
},
"FMLCommonHandler": {
"service": "PlatformService",
"file": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"reason": "Side detection is platform infrastructure",
},
"ServerConfigurationManager": {
"service": "PlatformService",
"file": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"reason": "Player management access is core platform concern",
},
"JsonToNBT": {
"service": "NBTIO",
"file": "platform-api/src/main/java/noppes/npcs/platform/nbt/NBTIO.java",
"reason": "JSON↔NBT conversion belongs with NBT infrastructure",
},
"StatCollector": {
"service": "PlatformService",
"file": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"reason": "Translation/localization is platform-specific",
},
}
# ============================================================
# SECTION 5: STATIC TRANSFORM PATTERNS
# Regex patterns for FORCE_TRANSFORM types.
# Used by Static-Transform.ps1 during migration waves.
#
# Format: "MCTypeName": [(regex_pattern, replacement), ...]
#
# HOW TO ADD: When you identify a new pattern for a TRANSFORM type,
# add the regex + replacement here. The classify command shows
# these patterns in its output.
# ============================================================
TRANSFORM_PATTERNS = {
"Side": [
(r"@SideOnly\s*\(\s*Side\.CLIENT\s*\)", "@ClientOnly"),
(r"@SideOnly\s*\(\s*Side\.SERVER\s*\)", "@ServerOnly"),
],
"SideOnly": [], # Handled by Side patterns above
"MathHelper": [
(r"MathHelper\.clamp_int\s*\(", "ValueUtil.clampInt("),
(r"MathHelper\.clamp_float\s*\(", "ValueUtil.clampFloat("),
(r"MathHelper\.clamp_double\s*\(", "ValueUtil.clampDouble("),
(r"MathHelper\.sqrt_double\s*\(", "Math.sqrt("),
(r"MathHelper\.sqrt_float\s*\(", "(float)Math.sqrt("),
(r"MathHelper\.floor_double\s*\(", "ValueUtil.floorDouble("),
(r"MathHelper\.floor_float\s*\(", "ValueUtil.floorFloat("),
],
"EnumChatFormatting": [
# Typically replaced with string constants or suppressed in core
# Add specific patterns as migration progresses
],
"EnumFacing": [
# Direction enum — add patterns when core code needs directions
],
}
# ============================================================
# SECTION 6: PARAMETER TYPE RESOLUTION
# When generating PA interface method signatures, MC types in
# parameters and return types need to be resolved to PA types.
#
# Strategy for types NOT in this map:
# "object" — use Object (pragmatic, safe, fast)
# "recursive" — also generate interfaces for param types (thorough)
# ============================================================
UNRESOLVED_PARAM_STRATEGY = "object"
# Known parameter type mappings: MC simple name → PA type name
# Used when generating interface method signatures.
PARAM_TYPE_MAP = {
# ---- Already abstracted (mirrors EXISTING_MAPPINGS by simple name) ----
"EntityPlayer": "IPlayer",
"EntityPlayerMP": "IPlayer",
"NBTTagCompound": "INbt",
"NBTTagList": "INbtList",
"ItemStack": "IItemStack",
"World": "IWorld",
"WorldServer": "IWorld",
"DamageSource": "IDamageSource",
"Entity": "IEntity",
"EntityLivingBase": "IEntityLivingBase",
"EntityLiving": "IEntityLiving",
# ---- Planned abstractions (use the NAMING value) ----
"ResourceLocation": "IResourceLocation",
"ChatComponentText": "ITextComponent",
"ChatComponentTranslation": "ITextComponent",
"IChatComponent": "ITextComponent",
"AxisAlignedBB": "IBoundingBox",
"Vec3": "IVector3",
"GameProfile": "IGameProfile",
"PotionEffect": "IPotionEffect",
"Potion": "IPotionType",
"Enchantment": "IEnchantment",
"ICommandSender": "ICommandSender",
"MovingObjectPosition": "IRayTraceResult",
# ---- Java primitives (pass through unchanged) ----
"String": "String",
"int": "int",
"float": "float",
"double": "double",
"boolean": "boolean",
"long": "long",
"byte": "byte",
"short": "short",
"char": "char",
"void": "void",
"byte[]": "byte[]",
"int[]": "int[]",
"String[]": "String[]",
"float[]": "float[]",
"double[]": "double[]",
# ---- Java standard library (pass through unchanged) ----
"List": "List",
"ArrayList": "List",
"Map": "Map",
"HashMap": "Map",
"Set": "Set",
"HashSet": "Set",
"Collection": "Collection",
"File": "File",
"UUID": "UUID",
"InputStream": "InputStream",
"OutputStream": "OutputStream",
"DataInputStream": "DataInputStream",
"DataOutputStream": "DataOutputStream",
"Object": "Object",
"Runnable": "Runnable",
}
# ============================================================
# SECTION 7: METHOD FILTERING
# Controls which methods are included/excluded in generated interfaces.
# ============================================================
# Methods to ALWAYS exclude from generated interfaces
# (inherited Object methods that never belong in PA interfaces)
EXCLUDED_METHODS = [
"toString",
"hashCode",
"equals",
"clone",
"finalize",
"getClass",
"notify",
"notifyAll",
"wait",
]
# Obfuscated MCP method names → readable names
# When surface-miner finds these, use the readable name in generated interfaces.
OBFUSCATED_METHOD_NAMES = {
"func_152457_a": "decompress",
"func_150296_c": "getTagType",
"func_150299_b": "getKeySet",
"func_74875_a": "getTagList",
"func_82580_o": "getShort",
"func_150295_c": "merge",
"func_74737_a": "copy",
"func_74732_a": "getId",
}
# If True, obfuscated methods (matching func_\d+_[a-z]) that are NOT in
# OBFUSCATED_METHOD_NAMES are excluded from generated interfaces.
EXCLUDE_UNMAPPED_OBFUSCATED = True
# ============================================================
# SECTION 8: JAVA CONSTRAINTS
# Compile-time constraints that affect generated code shape.
# ============================================================
# Java language level for generated interfaces.
# Java 7: no default methods, no static interface methods.
# Java 8: default and static methods allowed.
JAVA_TARGET = 7
# If True, add @Override on wrapper methods implementing PA interfaces.
ADD_OVERRIDE = True
# ============================================================
# SECTION 9: HELPER FUNCTIONS
# Convenience functions for consuming the rules above.
# These are the primary API for other modules.
# ============================================================
def _simple_name(fqn_or_simple):
"""Extract simple class name from FQN or return as-is."""
return fqn_or_simple.rsplit(".", 1)[-1] if "." in fqn_or_simple else fqn_or_simple
def is_existing(fqn):
"""Check if a fully-qualified MC type already has a PA abstraction."""
return fqn in EXISTING_MAPPINGS
def get_existing_pa(fqn):
"""Get the PA interface name for an already-abstracted MC type, or None."""
return EXISTING_MAPPINGS.get(fqn)
def classify_type(simple_name, surface_data=None):
"""
Classify an MC type into a migration category.
Args:
simple_name: MC class simple name (e.g. "MinecraftServer")
surface_data: Optional dict from surface-miner with keys:
"methods": [{"name": str, "kind": "static"|"instance", "count": int}, ...]
If provided, enables AUTO classification with heuristics.
Returns:
dict with keys:
"classification": "EXISTING"|"INTERFACE"|"SERVICE"|"TRANSFORM"|"SUPPRESS"|"AUTO"
"reason": str explaining why
"pa_name": str or None (the PA interface/service name)
"target": str or None (service file path for SERVICE types)
"transform_patterns": list or None (regex patterns for TRANSFORM types)
"""
name = _simple_name(simple_name)
# Check EXISTING first (by simple name match against EXISTING_MAPPINGS values)
for fqn, pa in EXISTING_MAPPINGS.items():
if _simple_name(fqn) == name:
return {
"classification": "EXISTING",
"reason": f"Already abstracted as {pa}",
"pa_name": pa,
"target": None,
"transform_patterns": None,
}
# Check SUPPRESS
if name in SUPPRESS:
return {
"classification": "SUPPRESS",
"reason": "Suppressed (client-only, debug, networking, or stays in mc1710)",
"pa_name": None,
"target": None,
"transform_patterns": None,
}
# Check FORCE_TRANSFORM
if name in FORCE_TRANSFORM:
patterns = TRANSFORM_PATTERNS.get(name, [])
return {
"classification": "TRANSFORM",
"reason": f"Static-Transform ({len(patterns)} regex patterns)",
"pa_name": None,
"target": None,
"transform_patterns": patterns,
}
# Check FORCE_SERVICE
if name in FORCE_SERVICE:
routing = SERVICE_ROUTING.get(name, {})
return {
"classification": "SERVICE",
"reason": routing.get("reason", "Force-classified as service"),
"pa_name": None,
"target": routing.get("file"),
"service_name": routing.get("service", "PlatformService"),
"transform_patterns": None,
}
# Check FORCE_INTERFACE
if name in FORCE_INTERFACE:
pa_name = get_pa_name(name)
return {
"classification": "INTERFACE",
"reason": "Force-classified as interface",
"pa_name": pa_name,
"target": _resolve_interface_path(pa_name),
"transform_patterns": None,
}
# AUTO classification using surface data (if provided)
if surface_data and surface_data.get("methods"):
methods = surface_data["methods"]
total = len(methods)
static_count = sum(1 for m in methods if m.get("kind") == "static")
static_ratio = static_count / total if total > 0 else 0
if total < MIN_METHODS_FOR_INTERFACE:
return {
"classification": "SERVICE",
"reason": f"Auto: only {total} methods (< {MIN_METHODS_FOR_INTERFACE} threshold)",
"pa_name": None,
"target": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"service_name": "PlatformService",
"transform_patterns": None,
}
if static_ratio > STATIC_THRESHOLD:
return {
"classification": "SERVICE",
"reason": f"Auto: {static_ratio:.0%} static methods (> {STATIC_THRESHOLD:.0%} threshold)",
"pa_name": None,
"target": "platform-api/src/main/java/kamkeel/npcs/platform/PlatformService.java",
"service_name": "PlatformService",
"transform_patterns": None,
}
pa_name = get_pa_name(name)
return {
"classification": "INTERFACE",
"reason": f"Auto: {total} methods, {static_ratio:.0%} static → instance-heavy",
"pa_name": pa_name,
"target": _resolve_interface_path(pa_name),
"transform_patterns": None,
}
# No data, no explicit rule → UNKNOWN
return {
"classification": "AUTO",
"reason": "Not classified. Run with --surface to enable auto-classification.",
"pa_name": get_pa_name(name),
"target": None,
"transform_patterns": None,
}
def get_pa_name(simple_name):
"""
Get the PA interface name for an MC type.
Uses NAMING overrides, falls back to "I" + simple_name.
"""
name = _simple_name(simple_name)
return NAMING.get(name, f"I{name}")
def get_service_target(simple_name):
"""
Get the service routing for a SERVICE-classified type.
Returns dict with 'service', 'file', 'reason' or None.
"""
name = _simple_name(simple_name)
return SERVICE_ROUTING.get(name)
def resolve_param_type(mc_type_name):
"""
Resolve an MC type name to its PA equivalent for use in method signatures.
Returns the PA type name, or "Object" if unresolved (per UNRESOLVED_PARAM_STRATEGY).
"""
name = _simple_name(mc_type_name)
resolved = PARAM_TYPE_MAP.get(name)
if resolved:
return resolved
if UNRESOLVED_PARAM_STRATEGY == "object":
return "Object"
return name # "recursive" strategy — caller handles generation
def should_exclude_method(method_name):
"""Check if a method should be excluded from generated interfaces."""
if method_name in EXCLUDED_METHODS:
return True
if EXCLUDE_UNMAPPED_OBFUSCATED and re.match(r"func_\d+_[a-z]", method_name):
return method_name not in OBFUSCATED_METHOD_NAMES
return False
def resolve_obfuscated_method(method_name):
"""Resolve an obfuscated MCP method name to readable, or return as-is."""
return OBFUSCATED_METHOD_NAMES.get(method_name, method_name)
def get_interface_package(pa_name):
"""Determine which package a PA interface should go in based on its name."""
for pattern, package in PACKAGE_ROUTING:
if re.search(pattern, pa_name):
return package
return DEFAULT_INTERFACE_PACKAGE
def _resolve_interface_path(pa_name):
"""Build the full file path for a PA interface."""
package = get_interface_package(pa_name)
package_path = package.replace(".", "/")
return f"platform-api/src/main/java/{package_path}/{pa_name}.java"
# ============================================================
# SECTION 10: QUICK REFERENCE
# Summary of all classified types for fast lookup.
# Run: python -m pa_rules to see the full classification table.
# ============================================================
if __name__ == "__main__":
print("=" * 72)
print("PA GENERATION RULES — CLASSIFICATION SUMMARY")
print("=" * 72)
print(f"\n EXISTING ({len(EXISTING_MAPPINGS)} types):")
for fqn, pa in sorted(EXISTING_MAPPINGS.items(), key=lambda x: x[1]):
print(f" {_simple_name(fqn):30s} → {pa}")
print(f"\n INTERFACE ({len(FORCE_INTERFACE)} types):")
for t in sorted(FORCE_INTERFACE):
pa = get_pa_name(t)
path = _resolve_interface_path(pa)
print(f" {t:30s} → {pa:20s} {path}")
print(f"\n SERVICE ({len(FORCE_SERVICE)} types):")
for t in sorted(FORCE_SERVICE):
routing = SERVICE_ROUTING.get(t, {})
svc = routing.get("service", "PlatformService")
print(f" {t:30s} → {svc}")
print(f"\n TRANSFORM ({len(FORCE_TRANSFORM)} types):")
for t in sorted(FORCE_TRANSFORM):
patterns = TRANSFORM_PATTERNS.get(t, [])
print(f" {t:30s} ({len(patterns)} patterns)")
print(f"\n SUPPRESS ({len(SUPPRESS)} types):")
for i in range(0, len(SUPPRESS), 6):
chunk = SUPPRESS[i : i + 6]
print(f" {', '.join(chunk)}")
total = (
len(EXISTING_MAPPINGS)
+ len(FORCE_INTERFACE)
+ len(FORCE_SERVICE)
+ len(FORCE_TRANSFORM)
+ len(SUPPRESS)
)
print(f"\n TOTAL CLASSIFIED: {total}")
print(f" REMAINING AUTO: {400 - total} (of ~400 MC types in codebase)")
print(f"\n Run 'python analyze.py classify <type>' for per-type classification.")
+780
View File
@@ -0,0 +1,780 @@
#!/usr/bin/env python3
"""
scan_codebase.py — Full MC/Forge Usage Index Builder
=====================================================
Uses tree-sitter for AST-accurate scanning of all Java files.
Produces a JSON index of every Minecraft/Forge type: imports, usage patterns,
method calls, field accesses, class hierarchy, and PA abstraction status.
Usage:
pip install tree-sitter tree-sitter-java
python tools/migration/scan_codebase.py
python tools/migration/scan_codebase.py --scan-dir mc1710/src/main/java --output mc_usage_index.json
python tools/migration/scan_codebase.py --top 30
"""
import os
import sys
import json
import re
import argparse
import time
from collections import defaultdict
from pathlib import Path
# ============================================================
# Configuration
# ============================================================
MC_PREFIXES = ("net.minecraft.", "cpw.mods.", "net.minecraftforge.")
from pa_rules import EXISTING_MAPPINGS as KNOWN_PA
# ============================================================
# Tree-sitter Setup
# ============================================================
def setup_parser():
"""Initialize tree-sitter parser with Java grammar."""
try:
import tree_sitter_java as tsjava
from tree_sitter import Language, Parser
except ImportError:
print("ERROR: Required packages not installed.")
print("Run: pip install tree-sitter tree-sitter-java")
sys.exit(1)
lang = Language(tsjava.language())
parser = Parser(lang)
return parser
# ============================================================
# Java File Analyzer
# ============================================================
class JavaFileAnalyzer:
"""Analyzes a single Java file for MC/Forge type usage via tree-sitter AST."""
def __init__(self, parser, source_bytes, rel_path):
self.source = source_bytes
self.rel_path = rel_path
self.tree = parser.parse(source_bytes)
self.root = self.tree.root_node
# Import maps
self.mc_imports = {} # simple_name → fqn (MC/Forge only)
# Variable → type mapping (flat per file, last-write-wins)
self.type_vars = {} # var_name → simple_type_name
# Extracted data
self.extends_list = [] # (our_class, mc_simple_name)
self.implements_list = [] # (our_class, mc_simple_name)
self.new_calls = [] # (mc_simple_name, line)
self.instanceof_checks = [] # (mc_simple_name, line)
self.casts = [] # (mc_simple_name, line)
self.method_calls = defaultdict(list) # mc_fqn → [(method_name, line)]
self.static_calls = defaultdict(list) # mc_fqn → [(method_name, line)]
self.field_accesses = defaultdict(list) # mc_fqn → [(field_name, line)]
def _text(self, node):
if node is None:
return None
return node.text.decode("utf-8")
def analyze(self):
self._extract_imports()
if self.mc_imports: # Skip AST walk if no MC imports
self._walk(self.root)
return self._build_result()
# --- Import extraction (regex — imports are trivially structured) ---
def _extract_imports(self):
text = self.source.decode("utf-8", errors="replace")
for line in text.split("\n"):
line = line.strip()
m = re.match(r"import\s+(static\s+)?([a-zA-Z0-9_.*]+)\s*;", line)
if not m:
continue
is_static = bool(m.group(1))
fqn = m.group(2)
if fqn.endswith(".*"):
continue # Wildcard — can't resolve simple names
if is_static:
# Static import: last part is method/field, parent is class
parts = fqn.rsplit(".", 1)
if len(parts) == 2:
class_fqn = parts[0]
simple = class_fqn.rsplit(".", 1)[-1]
if self._is_mc(class_fqn):
self.mc_imports[simple] = class_fqn
else:
simple = fqn.rsplit(".", 1)[-1]
if self._is_mc(fqn):
self.mc_imports[simple] = fqn
def _is_mc(self, fqn):
return any(fqn.startswith(p) for p in MC_PREFIXES)
# --- AST walking ---
def _walk(self, node):
self._process_node(node)
for child in node.children:
self._walk(child)
def _process_node(self, node):
t = node.type
if (
t == "class_declaration"
or t == "interface_declaration"
or t == "enum_declaration"
):
self._handle_class_decl(node)
elif t == "field_declaration":
self._handle_var_decl(node)
elif t == "local_variable_declaration":
self._handle_var_decl(node)
elif t == "formal_parameter":
self._handle_formal_param(node)
elif t == "enhanced_for_statement":
self._handle_enhanced_for(node)
elif t == "method_invocation":
self._handle_method_invocation(node)
elif t == "object_creation_expression":
self._handle_object_creation(node)
elif t == "cast_expression":
self._handle_cast(node)
elif t == "instanceof_expression":
self._handle_instanceof(node)
elif t == "field_access":
self._handle_field_access(node)
# --- Node handlers ---
def _handle_class_decl(self, node):
name_node = node.child_by_field_name("name")
class_name = self._text(name_node) or "?"
# Superclass: (superclass extends type_identifier)
sc = node.child_by_field_name("superclass")
if sc:
for child in sc.children:
tn = self._extract_type_name(child)
if tn and tn in self.mc_imports:
self.extends_list.append((class_name, tn))
# Interfaces: (super_interfaces implements type_list(type_identifier ...))
ifaces = node.child_by_field_name("interfaces")
if ifaces:
for child in self._iter_all(ifaces):
tn = self._extract_type_name(child)
if tn and tn in self.mc_imports:
self.implements_list.append((class_name, tn))
def _handle_var_decl(self, node):
"""Handle field_declaration and local_variable_declaration.
AST: type_identifier @type variable_declarator @declarator(identifier @name)"""
type_node = node.child_by_field_name("type")
type_name = self._extract_type_name(type_node)
if not type_name:
return
for child in node.children:
if child.type == "variable_declarator":
name_node = child.child_by_field_name("name")
if name_node:
self.type_vars[self._text(name_node)] = type_name
def _handle_formal_param(self, node):
"""AST: type_identifier @type identifier @name"""
type_node = node.child_by_field_name("type")
name_node = node.child_by_field_name("name")
type_name = self._extract_type_name(type_node)
var_name = self._text(name_node)
if type_name and var_name:
self.type_vars[var_name] = type_name
def _handle_enhanced_for(self, node):
"""AST: for ( type_identifier @type identifier @name : expr ) block"""
type_node = node.child_by_field_name("type")
name_node = node.child_by_field_name("name")
type_name = self._extract_type_name(type_node)
var_name = self._text(name_node)
if type_name and var_name:
self.type_vars[var_name] = type_name
def _handle_method_invocation(self, node):
"""AST: identifier @object . identifier @name argument_list @arguments
OR: field_access @object . identifier @name argument_list @arguments"""
obj_node = node.child_by_field_name("object")
name_node = node.child_by_field_name("name")
if not name_node or not obj_node:
return
method_name = self._text(name_node)
line = node.start_point[0] + 1
# Case 1: direct variable — player.getHealth()
if obj_node.type == "identifier":
receiver = self._text(obj_node)
# Check if it's a type name (static call): MinecraftServer.getServer()
if receiver in self.mc_imports and receiver[0:1].isupper():
self.static_calls[self.mc_imports[receiver]].append((method_name, line))
return
# Instance call: look up variable type
if receiver in self.type_vars:
type_name = self.type_vars[receiver]
if type_name in self.mc_imports:
self.method_calls[self.mc_imports[type_name]].append(
(method_name, line)
)
return
# Case 2: this.field.method() — field_access(this, field)
if obj_node.type == "field_access":
obj_obj = obj_node.child_by_field_name("object")
field = obj_node.child_by_field_name("field")
if obj_obj and obj_obj.type == "this" and field:
receiver = self._text(field)
if receiver in self.type_vars:
type_name = self.type_vars[receiver]
if type_name in self.mc_imports:
self.method_calls[self.mc_imports[type_name]].append(
(method_name, line)
)
def _handle_object_creation(self, node):
"""AST: new type_identifier @type argument_list @arguments"""
type_node = node.child_by_field_name("type")
type_name = self._extract_type_name(type_node)
if type_name and type_name in self.mc_imports:
self.new_calls.append((type_name, node.start_point[0] + 1))
def _handle_cast(self, node):
"""AST: ( type_identifier @type ) expression @value"""
type_node = node.child_by_field_name("type")
type_name = self._extract_type_name(type_node)
if type_name and type_name in self.mc_imports:
self.casts.append((type_name, node.start_point[0] + 1))
def _handle_instanceof(self, node):
"""AST: expression @left instanceof type_identifier @right"""
right = node.child_by_field_name("right")
type_name = self._extract_type_name(right)
if type_name and type_name in self.mc_imports:
self.instanceof_checks.append((type_name, node.start_point[0] + 1))
def _handle_field_access(self, node):
"""AST: identifier @object . identifier @field
Only records when the receiver is a variable with MC type AND
this field_access is NOT the object of a method_invocation (those are
handled by method_invocation handler)."""
# Skip if this is the object of a method call (method handler covers it)
parent = node.parent
if parent and parent.type == "method_invocation":
p_obj = parent.child_by_field_name("object")
if p_obj is not None and p_obj.id == node.id:
return
obj_node = node.child_by_field_name("object")
field_node = node.child_by_field_name("field")
if not obj_node or not field_node:
return
field_name = self._text(field_node)
line = node.start_point[0] + 1
receiver = None
if obj_node.type == "identifier":
receiver = self._text(obj_node)
elif obj_node.type == "this":
return # Can't resolve this.field without class context
if receiver and receiver in self.type_vars:
type_name = self.type_vars[receiver]
if type_name in self.mc_imports:
self.field_accesses[self.mc_imports[type_name]].append(
(field_name, line)
)
# --- Type name extraction ---
def _extract_type_name(self, node):
"""Extract the simple type name from any type AST node."""
if node is None:
return None
t = node.type
if t == "type_identifier":
return self._text(node)
elif t == "scoped_type_identifier":
# e.g. NBTBase.NBTPrimitive — return first type_identifier
for child in node.children:
if child.type == "type_identifier":
return self._text(child)
return self._text(node)
elif t == "generic_type":
# e.g. List<EntityPlayer> — return the base type
for child in node.children:
if child.type in ("type_identifier", "scoped_type_identifier"):
return self._extract_type_name(child)
elif t == "array_type":
# e.g. EntityPlayer[] — return element type
el = node.child_by_field_name("element")
if el:
return self._extract_type_name(el)
for child in node.children:
r = self._extract_type_name(child)
if r:
return r
return None
def _iter_all(self, node):
"""Recursively yield all descendant nodes."""
for child in node.children:
yield child
yield from self._iter_all(child)
# --- Build result ---
def _build_result(self):
return {
"mc_imports": dict(self.mc_imports),
"extends": self.extends_list,
"implements": self.implements_list,
"new_calls": self.new_calls,
"instanceof": self.instanceof_checks,
"casts": self.casts,
"method_calls": {k: list(v) for k, v in self.method_calls.items()},
"static_calls": {k: list(v) for k, v in self.static_calls.items()},
"field_accesses": {k: list(v) for k, v in self.field_accesses.items()},
}
# ============================================================
# Index Builder (aggregates across all files)
# ============================================================
class IndexBuilder:
def __init__(self, project_root, scan_dir, parser):
self.project_root = Path(project_root)
self.scan_dir = self.project_root / scan_dir
self.parser = parser
# Per-type aggregation
self.type_data = {} # fqn → dict
self.file_data = {} # rel_path → dict
self.stats = {
"totalFiles": 0,
"filesWithMcDeps": 0,
"totalMcImports": 0,
"parseErrors": 0,
}
def _ensure_type(self, fqn, short_name):
if fqn not in self.type_data:
self.type_data[fqn] = {
"shortName": short_name,
"package": fqn.rsplit(".", 1)[0] if "." in fqn else "",
"importCount": 0,
"importedBy": [],
"paAbstraction": KNOWN_PA.get(fqn),
"usage": {
"extends": [],
"implements": [],
"constructors": [],
"casts": [],
"instanceof": [],
},
"methodCalls": {},
"staticCalls": {},
"fieldAccesses": {},
}
return self.type_data[fqn]
def _add_call(self, store, method_name, rel_path):
if method_name not in store:
store[method_name] = {"count": 0, "files": []}
entry = store[method_name]
entry["count"] += 1
if rel_path not in entry["files"]:
entry["files"].append(rel_path)
def scan(self):
java_files = []
for root, _dirs, files in os.walk(self.scan_dir):
for f in files:
if f.endswith(".java"):
java_files.append(Path(root) / f)
self.stats["totalFiles"] = len(java_files)
t0 = time.time()
for i, filepath in enumerate(java_files):
rel_path = filepath.relative_to(self.scan_dir).as_posix()
try:
source = filepath.read_bytes()
analyzer = JavaFileAnalyzer(self.parser, source, rel_path)
result = analyzer.analyze()
self._aggregate(rel_path, result)
except Exception as e:
self.stats["parseErrors"] += 1
print(f" WARN: {rel_path}: {e}", file=sys.stderr)
# Progress
if (i + 1) % 500 == 0:
print(f" ... {i + 1}/{len(java_files)} files", file=sys.stderr)
elapsed = time.time() - t0
print(
f" Scanned {len(java_files)} files in {elapsed:.1f}s "
f"({self.stats['parseErrors']} errors)",
file=sys.stderr,
)
def _aggregate(self, rel_path, result):
mc_imports = result["mc_imports"]
if not mc_imports:
return
self.stats["filesWithMcDeps"] += 1
self.stats["totalMcImports"] += len(mc_imports)
# File index
self.file_data[rel_path] = {
"mcImportCount": len(mc_imports),
"mcTypes": sorted(mc_imports.keys()),
"mcFqns": sorted(set(mc_imports.values())),
}
# Type index — imports
for simple, fqn in mc_imports.items():
ti = self._ensure_type(fqn, simple)
ti["importCount"] += 1
ti["importedBy"].append(rel_path)
# Hierarchy
for class_name, mc_simple in result["extends"]:
if mc_simple in mc_imports:
fqn = mc_imports[mc_simple]
self._ensure_type(fqn, mc_simple)["usage"]["extends"].append(
{"file": rel_path, "class": class_name}
)
for class_name, mc_simple in result["implements"]:
if mc_simple in mc_imports:
fqn = mc_imports[mc_simple]
self._ensure_type(fqn, mc_simple)["usage"]["implements"].append(
{"file": rel_path, "class": class_name}
)
# Constructors
for mc_simple, line in result["new_calls"]:
if mc_simple in mc_imports:
fqn = mc_imports[mc_simple]
self._ensure_type(fqn, mc_simple)["usage"]["constructors"].append(
{"file": rel_path, "line": line}
)
# Casts
for mc_simple, line in result["casts"]:
if mc_simple in mc_imports:
fqn = mc_imports[mc_simple]
self._ensure_type(fqn, mc_simple)["usage"]["casts"].append(
{"file": rel_path, "line": line}
)
# instanceof
for mc_simple, line in result["instanceof"]:
if mc_simple in mc_imports:
fqn = mc_imports[mc_simple]
self._ensure_type(fqn, mc_simple)["usage"]["instanceof"].append(
{"file": rel_path, "line": line}
)
# Method calls
for fqn, calls in result["method_calls"].items():
ti = self._ensure_type(fqn, fqn.rsplit(".", 1)[-1])
for method_name, _line in calls:
self._add_call(ti["methodCalls"], method_name, rel_path)
# Static calls
for fqn, calls in result["static_calls"].items():
ti = self._ensure_type(fqn, fqn.rsplit(".", 1)[-1])
for method_name, _line in calls:
self._add_call(ti["staticCalls"], method_name, rel_path)
# Field accesses
for fqn, accesses in result["field_accesses"].items():
ti = self._ensure_type(fqn, fqn.rsplit(".", 1)[-1])
for field_name, _line in accesses:
self._add_call(ti["fieldAccesses"], field_name, rel_path)
def build_output(self, top_n=50):
sorted_types = sorted(
self.type_data.items(), key=lambda x: x[1]["importCount"], reverse=True
)
# Per-package stats
by_package = defaultdict(lambda: {"types": set(), "totalImports": 0})
abstracted = 0
not_abstracted = 0
for fqn, ti in sorted_types:
pkg = ti["package"]
by_package[pkg]["types"].add(ti["shortName"])
by_package[pkg]["totalImports"] += ti["importCount"]
if ti["paAbstraction"]:
abstracted += 1
else:
not_abstracted += 1
total_types = abstracted + not_abstracted
# Hierarchy rollup
hierarchy_extends = defaultdict(list)
hierarchy_implements = defaultdict(list)
for fqn, ti in sorted_types:
for entry in ti["usage"]["extends"]:
hierarchy_extends[fqn].append(entry)
for entry in ti["usage"]["implements"]:
hierarchy_implements[fqn].append(entry)
summary = {
"totalFiles": self.stats["totalFiles"],
"filesWithMcDeps": self.stats["filesWithMcDeps"],
"totalMcImports": self.stats["totalMcImports"],
"uniqueMcTypes": len(self.type_data),
"parseErrors": self.stats["parseErrors"],
"abstractionCoverage": {
"abstracted": abstracted,
"notAbstracted": not_abstracted,
"total": total_types,
"percentage": f"{abstracted / total_types * 100:.1f}%"
if total_types > 0
else "0%",
},
"topTypes": [
{
"fqn": fqn,
"shortName": ti["shortName"],
"importCount": ti["importCount"],
"pa": ti["paAbstraction"],
"methodCallCount": sum(
m["count"] for m in ti["methodCalls"].values()
),
"staticCallCount": sum(
m["count"] for m in ti["staticCalls"].values()
),
"fieldAccessCount": sum(
m["count"] for m in ti["fieldAccesses"].values()
),
"extendsCount": len(ti["usage"]["extends"]),
"implementsCount": len(ti["usage"]["implements"]),
"constructorCount": len(ti["usage"]["constructors"]),
}
for fqn, ti in sorted_types[:top_n]
],
"topFiles": sorted(
[
{"file": f, "mcImports": d["mcImportCount"]}
for f, d in self.file_data.items()
],
key=lambda x: x["mcImports"],
reverse=True,
)[:top_n],
"byPackage": {
pkg: {
"types": len(data["types"]),
"typeNames": sorted(data["types"]),
"totalImports": data["totalImports"],
}
for pkg, data in sorted(
by_package.items(), key=lambda x: x[1]["totalImports"], reverse=True
)
},
}
return {
"meta": {
"scanDir": str(self.scan_dir.relative_to(self.project_root)),
"projectRoot": str(self.project_root),
"generatedBy": "scan_codebase.py (tree-sitter)",
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
},
"summary": summary,
"types": {fqn: ti for fqn, ti in sorted_types},
"files": self.file_data,
"hierarchy": {
"extends": dict(hierarchy_extends),
"implements": dict(hierarchy_implements),
},
}
# ============================================================
# Console Summary
# ============================================================
def print_summary(output):
s = output["summary"]
cov = s["abstractionCoverage"]
print()
print("=" * 72)
print(f" MC/FORGE USAGE INDEX — {output['meta']['scanDir']}")
print("=" * 72)
print(f" Total files scanned: {s['totalFiles']:>6,}")
print(f" Files with MC deps: {s['filesWithMcDeps']:>6,}")
print(f" Total MC imports: {s['totalMcImports']:>6,}")
print(f" Unique MC types: {s['uniqueMcTypes']:>6}")
print(f" Parse errors: {s['parseErrors']:>6}")
print(
f" PA coverage: {cov['abstracted']}/{cov['total']} ({cov['percentage']})"
)
print()
# Top types
print(" TOP MC TYPES BY IMPORT COUNT")
print(" " + "-" * 68)
fmt = " {rank:>3}. {name:<30} {imp:>4} imports {methods:>4} methods {fields:>3} fields {pa}"
for i, t in enumerate(s["topTypes"][:30]):
pa_str = f"-> {t['pa']}" if t["pa"] else ""
print(
fmt.format(
rank=i + 1,
name=t["shortName"],
imp=t["importCount"],
methods=t["methodCallCount"] + t["staticCallCount"],
fields=t["fieldAccessCount"],
pa=pa_str,
)
)
print()
# Top packages
print(" TOP MC PACKAGES")
print(" " + "-" * 68)
for pkg, data in list(s["byPackage"].items())[:15]:
print(
f" {pkg:<45} {data['types']:>3} types {data['totalImports']:>5} imports"
)
print()
# Top files
print(" TOP FILES BY MC DEPENDENCY COUNT")
print(" " + "-" * 68)
for f in s["topFiles"][:15]:
print(f" {f['file']:<55} {f['mcImports']:>3} imports")
print()
# Hierarchy summary
ext = output["hierarchy"]["extends"]
impl = output["hierarchy"]["implements"]
if ext:
print(
f" CLASSES EXTENDING MC TYPES ({sum(len(v) for v in ext.values())} total)"
)
print(" " + "-" * 68)
for fqn, entries in sorted(ext.items(), key=lambda x: len(x[1]), reverse=True)[
:10
]:
short = fqn.rsplit(".", 1)[-1]
classes = [e["class"] for e in entries]
print(f" {short:<30} <- {', '.join(classes[:5])}")
if len(classes) > 5:
print(f" {'':30} ... and {len(classes) - 5} more")
print()
if impl:
print(
f" CLASSES IMPLEMENTING MC INTERFACES ({sum(len(v) for v in impl.values())} total)"
)
print(" " + "-" * 68)
for fqn, entries in sorted(impl.items(), key=lambda x: len(x[1]), reverse=True)[
:10
]:
short = fqn.rsplit(".", 1)[-1]
classes = [e["class"] for e in entries]
print(f" {short:<30} <- {', '.join(classes[:5])}")
if len(classes) > 5:
print(f" {'':30} ... and {len(classes) - 5} more")
print()
print("=" * 72)
# ============================================================
# Main
# ============================================================
def main():
# Auto-detect project root from script location
script_dir = Path(__file__).resolve().parent
default_root = script_dir.parent.parent # tools/migration/ -> project root
ap = argparse.ArgumentParser(
description="MC/Forge Usage Index Builder (tree-sitter)"
)
ap.add_argument(
"--scan-dir",
default="mc1710/src/main/java",
help="Directory to scan (relative to project root)",
)
ap.add_argument(
"--output",
"-o",
default="mc_usage_index.json",
help="Output JSON file path (relative to project root)",
)
ap.add_argument(
"--project-root", default=str(default_root), help="Project root directory"
)
ap.add_argument(
"--top", type=int, default=50, help="Number of top entries in summary"
)
ap.add_argument(
"--quiet", "-q", action="store_true", help="Suppress console summary"
)
args = ap.parse_args()
project_root = Path(args.project_root).resolve()
output_path = project_root / args.output
print(f"Initializing tree-sitter parser...", file=sys.stderr)
parser = setup_parser()
print(f"Scanning {args.scan_dir}...", file=sys.stderr)
builder = IndexBuilder(project_root, args.scan_dir, parser)
builder.scan()
print(f"Building index...", file=sys.stderr)
output = builder.build_output(top_n=args.top)
# Write JSON
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
size_kb = output_path.stat().st_size / 1024
print(f"Wrote {output_path} ({size_kb:.0f} KB)", file=sys.stderr)
# Console summary
if not args.quiet:
print_summary(output)
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
+637
View File
@@ -0,0 +1,637 @@
#!/usr/bin/env python3
"""Test suite for pa_editor.py — validates all operation types and error handling.
Run: python tools/migration/test_pa_editor.py
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PA_EDITOR = os.path.join(SCRIPT_DIR, "pa_editor.py")
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
PASS = 0
FAIL = 0
def test(name: str, passed: bool, detail: str = ""):
global PASS, FAIL
if passed:
PASS += 1
print(f" PASS {name}")
else:
FAIL += 1
print(f" FAIL {name}" + (f" — {detail}" if detail else ""))
def run_editor(manifest_path: str, *args: str) -> tuple[int, str, str]:
cmd = [
sys.executable,
PA_EDITOR,
manifest_path,
*args,
"--project-root",
PROJECT_ROOT,
]
result = subprocess.run(cmd, capture_output=True, text=True, cwd=PROJECT_ROOT)
return result.returncode, result.stdout, result.stderr
def write_file(path: str, content: str):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(content)
def read_file(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read()
def write_manifest(tmpdir: str, manifest: dict) -> str:
path = os.path.join(tmpdir, "manifest.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=2)
return path
SAMPLE_JAVA = """\
package test;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.entity.player.EntityPlayer;
import java.util.List;
public class Sample {
private int id;
public void load(NBTTagCompound nbt) {
id = nbt.getInteger("id");
}
public void interact(EntityPlayer player) {
player.addChatMessage(null);
}
}
"""
def run_tests():
tmpdir = tempfile.mkdtemp(
prefix="pa_editor_test_", dir=os.path.join(PROJECT_ROOT, "tools", "migration")
)
try:
_run_all(tmpdir)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
print(f"\n{'=' * 50}")
print(f" RESULTS: {PASS} passed, {FAIL} failed")
print(f"{'=' * 50}")
return FAIL == 0
def _run_all(tmpdir: str):
print("\n=== PA Editor Test Suite ===\n")
# ----------------------------------------------------------------
# TEST 1: Replace by pattern
# ----------------------------------------------------------------
f1 = os.path.join(tmpdir, "Replace.java")
write_file(f1, SAMPLE_JAVA)
rel1 = os.path.relpath(f1, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel1,
"ops": [
{
"op": "replace",
"target": {
"pattern": "^import net\\.minecraft\\.nbt\\.NBTTagCompound;"
},
"content": "import api.INbt;",
},
],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("replace-by-pattern: exit 0", code == 0)
content = read_file(f1)
test("replace-by-pattern: import changed", "import api.INbt;" in content)
test(
"replace-by-pattern: old import gone",
"import net.minecraft.nbt.NBTTagCompound;" not in content,
)
# ----------------------------------------------------------------
# TEST 2: Insert before/after
# ----------------------------------------------------------------
f2 = os.path.join(tmpdir, "Insert.java")
write_file(f2, SAMPLE_JAVA)
rel2 = os.path.relpath(f2, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel2,
"ops": [
{
"op": "insert",
"target": {"line": 1},
"position": "before",
"content": "// Header comment",
},
{
"op": "insert",
"target": {"pattern": "^public class"},
"position": "after",
"content": " // class body start",
},
],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("insert: exit 0", code == 0)
content = read_file(f2)
lines = content.split("\n")
test("insert-before: first line is comment", lines[0] == "// Header comment")
test(
"insert-after: class body comment exists", " // class body start" in content
)
# ----------------------------------------------------------------
# TEST 3: Delete lines
# ----------------------------------------------------------------
f3 = os.path.join(tmpdir, "Delete.java")
write_file(f3, SAMPLE_JAVA)
rel3 = os.path.relpath(f3, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel3,
"ops": [
{
"op": "delete",
"target": {"pattern": "^import net\\.minecraft\\.nbt"},
"count": 1,
},
{
"op": "delete",
"target": {"pattern": "^import net\\.minecraft\\.entity"},
"count": 1,
},
],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("delete: exit 0", code == 0)
content = read_file(f3)
test("delete: MC imports removed", "net.minecraft" not in content)
test("delete: java import preserved", "java.util.List" in content)
# ----------------------------------------------------------------
# TEST 4: Multi-line insert
# ----------------------------------------------------------------
f4 = os.path.join(tmpdir, "MultiInsert.java")
write_file(f4, SAMPLE_JAVA)
rel4 = os.path.relpath(f4, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel4,
"ops": [
{
"op": "insert",
"target": {"pattern": "^public class"},
"position": "before",
"content": "/**\n * Migrated class.\n * PA types only.\n */",
},
],
}
]
},
)
code, _, _ = run_editor(manifest, "--force")
test("multi-line-insert: exit 0", code == 0)
content = read_file(f4)
test("multi-line-insert: javadoc present", "* Migrated class." in content)
test("multi-line-insert: javadoc 3 lines", "* PA types only." in content)
# ----------------------------------------------------------------
# TEST 5: Multi-line replace (count > 1)
# ----------------------------------------------------------------
f5 = os.path.join(tmpdir, "MultiReplace.java")
write_file(f5, SAMPLE_JAVA)
rel5 = os.path.relpath(f5, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel5,
"ops": [
{
"op": "replace",
"target": {"pattern": "^import net\\.minecraft\\.nbt"},
"count": 2,
"content": "import api.INbt;\nimport api.IPlayer;",
},
],
}
]
},
)
code, _, _ = run_editor(manifest, "--force")
test("multi-replace: exit 0", code == 0)
content = read_file(f5)
test(
"multi-replace: new imports present",
"import api.INbt;" in content and "import api.IPlayer;" in content,
)
test("multi-replace: old imports gone", "net.minecraft" not in content)
# ----------------------------------------------------------------
# TEST 6: Dry-run does not write
# ----------------------------------------------------------------
f6 = os.path.join(tmpdir, "DryRun.java")
write_file(f6, SAMPLE_JAVA)
rel6 = os.path.relpath(f6, PROJECT_ROOT)
original = read_file(f6)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel6,
"ops": [
{
"op": "replace",
"target": {"line": 1},
"content": "// MODIFIED",
}
],
}
]
},
)
code, out, _ = run_editor(manifest, "--dry-run")
test("dry-run: exit 0", code == 0)
test("dry-run: file unchanged", read_file(f6) == original)
test("dry-run: output shows DRY RUN", "DRY RUN" in out)
# ----------------------------------------------------------------
# TEST 7: Dry-run JSON output
# ----------------------------------------------------------------
code, out, _ = run_editor(manifest, "--dry-run", "--json")
test("dry-run-json: exit 0", code == 0)
data = json.loads(out)
test("dry-run-json: has diffs", "diffs" in data)
test("dry-run-json: dry_run flag true", data.get("dry_run") is True)
# ----------------------------------------------------------------
# TEST 8: Backup creates .bak files
# ----------------------------------------------------------------
f8 = os.path.join(tmpdir, "Backup.java")
write_file(f8, SAMPLE_JAVA)
rel8 = os.path.relpath(f8, PROJECT_ROOT)
original = read_file(f8)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel8,
"ops": [
{
"op": "replace",
"target": {"line": 1},
"content": "// MODIFIED",
}
],
}
]
},
)
code, _, _ = run_editor(manifest, "--force", "--backup")
test("backup: exit 0", code == 0)
test("backup: .bak exists", os.path.isfile(f8 + ".bak"))
test("backup: .bak has original content", read_file(f8 + ".bak") == original)
# ----------------------------------------------------------------
# TEST 9: Error — file not found
# ----------------------------------------------------------------
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": os.path.join(tmpdir, "NONEXISTENT.java"),
"ops": [{"op": "replace", "target": {"line": 1}, "content": "x"}],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("error-file-not-found: exit 1", code == 1)
test("error-file-not-found: error in output", "file not found" in out)
# ----------------------------------------------------------------
# TEST 10: Error — line out of bounds
# ----------------------------------------------------------------
f10 = os.path.join(tmpdir, "OOB.java")
write_file(f10, SAMPLE_JAVA)
rel10 = os.path.relpath(f10, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel10,
"ops": [{"op": "delete", "target": {"line": 9999}, "count": 1}],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("error-oob: exit 1", code == 1)
test("error-oob: error mentions bounds", "out of bounds" in out)
# ----------------------------------------------------------------
# TEST 11: Error — pattern not found
# ----------------------------------------------------------------
f11 = os.path.join(tmpdir, "PatternFail.java")
write_file(f11, SAMPLE_JAVA)
rel11 = os.path.relpath(f11, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel11,
"ops": [
{
"op": "replace",
"target": {"pattern": "WILL_NEVER_MATCH"},
"content": "x",
}
],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("error-pattern-not-found: exit 1", code == 1)
test("error-pattern-not-found: error mentions pattern", "not found" in out)
# ----------------------------------------------------------------
# TEST 12: Error — invalid regex
# ----------------------------------------------------------------
f12 = os.path.join(tmpdir, "BadRegex.java")
write_file(f12, SAMPLE_JAVA)
rel12 = os.path.relpath(f12, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel12,
"ops": [
{
"op": "replace",
"target": {"pattern": "[invalid"},
"content": "x",
}
],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("error-bad-regex: exit 1", code == 1)
test("error-bad-regex: error mentions regex", "invalid regex" in out)
# ----------------------------------------------------------------
# TEST 13: Atomicity — partial failure prevents ALL writes
# ----------------------------------------------------------------
f13a = os.path.join(tmpdir, "Atomic_A.java")
f13b = os.path.join(tmpdir, "Atomic_B.java")
write_file(f13a, SAMPLE_JAVA)
write_file(f13b, SAMPLE_JAVA)
orig_a = read_file(f13a)
rel13a = os.path.relpath(f13a, PROJECT_ROOT)
rel13b = os.path.relpath(f13b, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel13a,
"ops": [
{
"op": "insert",
"target": {"line": 1},
"position": "before",
"content": "// SHOULD NOT APPEAR",
}
],
},
{
"path": rel13b,
"ops": [
{
"op": "replace",
"target": {"pattern": "WILL_NEVER_MATCH"},
"content": "x",
}
],
},
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("atomicity: exit 1", code == 1)
test("atomicity: file A unchanged", read_file(f13a) == orig_a, read_file(f13a)[:50])
# ----------------------------------------------------------------
# TEST 14: Multi-file success
# ----------------------------------------------------------------
f14a = os.path.join(tmpdir, "Multi_A.java")
f14b = os.path.join(tmpdir, "Multi_B.java")
f14c = os.path.join(tmpdir, "Multi_C.java")
write_file(f14a, SAMPLE_JAVA)
write_file(f14b, SAMPLE_JAVA)
write_file(f14c, SAMPLE_JAVA)
rel14a = os.path.relpath(f14a, PROJECT_ROOT)
rel14b = os.path.relpath(f14b, PROJECT_ROOT)
rel14c = os.path.relpath(f14c, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel14a,
"ops": [
{
"op": "replace",
"target": {"pattern": "^import net\\.minecraft\\.nbt"},
"content": "import api.INbt;",
}
],
},
{
"path": rel14b,
"ops": [
{
"op": "delete",
"target": {"pattern": "^import net\\.minecraft\\.entity"},
"count": 1,
}
],
},
{
"path": rel14c,
"ops": [
{
"op": "insert",
"target": {"line": 1},
"position": "before",
"content": "// File C header",
}
],
},
]
},
)
code, out, _ = run_editor(manifest, "--force", "--json")
test("multi-file: exit 0", code == 0)
data = json.loads(out)
test("multi-file: 3 files modified", data.get("files_modified") == 3)
test("multi-file A: import replaced", "import api.INbt;" in read_file(f14a))
test(
"multi-file B: import deleted",
"import net.minecraft.entity.player.EntityPlayer;" not in read_file(f14b),
)
test("multi-file C: header added", read_file(f14c).startswith("// File C header"))
# ----------------------------------------------------------------
# TEST 15: Pattern occurrence targeting
# ----------------------------------------------------------------
f15 = os.path.join(tmpdir, "Occurrence.java")
write_file(f15, "line A\nline B\nline A\nline C\nline A\n")
rel15 = os.path.relpath(f15, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel15,
"ops": [
{
"op": "replace",
"target": {"pattern": "^line A", "occurrence": 2},
"content": "line A_REPLACED",
}
],
}
]
},
)
code, _, _ = run_editor(manifest, "--force")
test("occurrence: exit 0", code == 0)
lines = read_file(f15).split("\n")
test("occurrence: line 1 unchanged", lines[0] == "line A")
test("occurrence: line 3 replaced", lines[2] == "line A_REPLACED")
test("occurrence: line 5 unchanged", lines[4] == "line A")
# ----------------------------------------------------------------
# TEST 16: Content as list of strings
# ----------------------------------------------------------------
f16 = os.path.join(tmpdir, "ListContent.java")
write_file(f16, SAMPLE_JAVA)
rel16 = os.path.relpath(f16, PROJECT_ROOT)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": rel16,
"ops": [
{
"op": "insert",
"target": {"line": 1},
"position": "before",
"content": ["// Line 1", "// Line 2", "// Line 3"],
}
],
}
]
},
)
code, _, _ = run_editor(manifest, "--force")
test("list-content: exit 0", code == 0)
lines = read_file(f16).split("\n")
test(
"list-content: 3 lines inserted",
lines[0] == "// Line 1" and lines[1] == "// Line 2" and lines[2] == "// Line 3",
)
# ----------------------------------------------------------------
# TEST 17: Structural validation (missing fields)
# ----------------------------------------------------------------
manifest = write_manifest(
tmpdir, {"edits": [{"path": "x.java", "ops": [{"op": "insert"}]}]}
)
code, out, _ = run_editor(manifest, "--force")
test("validation: missing content/target", code == 1)
manifest = write_manifest(
tmpdir,
{
"edits": [
{
"path": "x.java",
"ops": [{"op": "bogus", "target": {"line": 1}, "content": "x"}],
}
]
},
)
code, out, _ = run_editor(manifest, "--force")
test("validation: invalid op name", code == 1)
if __name__ == "__main__":
ok = run_tests()
sys.exit(0 if ok else 1)
+382
View File
@@ -0,0 +1,382 @@
# Test runner for all migration tools
# Run: powershell -ExecutionPolicy Bypass -File tools/migration/test_runner.ps1
$ErrorActionPreference = 'Stop'
$passed = 0
$failed = 0
function Reset-Sandbox {
$sandboxDir = Join-Path $PSScriptRoot 'test_sandbox\src'
# TestController.java
$dir1 = Join-Path $sandboxDir 'noppes\npcs\controllers'
if (!(Test-Path $dir1)) { New-Item -ItemType Directory -Path $dir1 -Force | Out-Null }
[IO.File]::WriteAllText("$dir1\TestController.java", @'
package noppes.npcs.controllers;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.entity.player.EntityPlayer;
import cpw.mods.fml.common.event.FMLServerStartingEvent;
import net.minecraftforge.common.MinecraftForge;
import noppes.npcs.controllers.data.TestData;
import noppes.npcs.util.TestUtil;
import java.util.HashMap;
import java.util.List;
public class TestController {
private HashMap<String, NBTTagCompound> data = new HashMap<>();
private NBTTagList tagList;
public void save(EntityPlayer player) {
NBTTagCompound compound = new NBTTagCompound();
compound.setString("name", player.getCommandSenderName());
CompressedStreamTools.writeCompressed(compound, outputStream);
}
public void load() {
NBTTagCompound compound = CompressedStreamTools.readCompressed(inputStream);
MathHelper.clamp_int(value, 0, 100);
}
}
'@)
# TestData.java
$dir2 = Join-Path $sandboxDir 'noppes\npcs\controllers\data'
if (!(Test-Path $dir2)) { New-Item -ItemType Directory -Path $dir2 -Force | Out-Null }
[IO.File]::WriteAllText("$dir2\TestData.java", @'
package noppes.npcs.controllers.data;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.ItemStack;
import noppes.npcs.controllers.TestController;
import noppes.npcs.util.TestUtil;
import java.util.ArrayList;
public class TestData {
private NBTTagCompound nbt;
private EntityPlayer player;
public NBTTagCompound writeNBT(NBTTagCompound compound) {
NBTTagList list = new NBTTagList();
NBTTagString str = new NBTTagString("hello");
compound.setTag("list", list);
return compound;
}
public void readNBT(NBTTagCompound compound) {
NBTTagList list = compound.getTagList("list", 10);
}
public ItemStack getItem() {
return null;
}
}
'@)
# TestUtil.java (with duplicate imports)
$dir3 = Join-Path $sandboxDir 'noppes\npcs\util'
if (!(Test-Path $dir3)) { New-Item -ItemType Directory -Path $dir3 -Force | Out-Null }
[IO.File]::WriteAllText("$dir3\TestUtil.java", @'
package noppes.npcs.util;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.controllers.TestController;
import noppes.npcs.controllers.TestController;
import java.util.Map;
public class TestUtil {
public static NBTTagCompound merge(NBTTagCompound a, NBTTagCompound b) {
NBTTagCompound result = new NBTTagCompound();
return result;
}
}
'@)
}
function Assert-Contains {
param([string]$Content, [string]$Expected, [string]$TestName)
if ($Content -match [regex]::Escape($Expected)) {
return $true
} else {
Write-Host " FAIL: Expected '$Expected' in $TestName" -ForegroundColor Red
return $false
}
}
function Assert-NotContains {
param([string]$Content, [string]$Unexpected, [string]$TestName)
if ($Content -match [regex]::Escape($Unexpected)) {
Write-Host " FAIL: Did NOT expect '$Unexpected' in $TestName" -ForegroundColor Red
return $false
} else {
return $true
}
}
# ===== Load all tools =====
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Static-Transform.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')
. (Join-Path $PSScriptRoot 'Create-And-Propagate.ps1')
$sandboxRel = 'tools/migration/test_sandbox/src'
$sandboxAbs = Join-Path $PSScriptRoot 'test_sandbox\src'
# ============================================================
Write-Host "`n========== TEST 1: Symbol-Swap ==========" -ForegroundColor Cyan
Reset-Sandbox
$result = Symbol-Swap -Directory $sandboxRel -Swaps @(
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.nbt.INbt'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.nbt.INbtList'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer'),
@('ItemStack', 'IItemStack', 'net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack')
)
$tc = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\TestController.java")
$td = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\data\TestData.java")
$ok = $true
$ok = (Assert-Contains $tc 'import noppes.npcs.api.nbt.INbt;' 'TestController new import') -and $ok
$ok = (Assert-NotContains $tc 'import net.minecraft.nbt.NBTTagCompound;' 'TestController old import removed') -and $ok
$ok = (Assert-Contains $tc 'HashMap<String, INbt>' 'TestController symbol swap') -and $ok
$ok = (Assert-Contains $td 'import noppes.npcs.api.entity.IPlayer;' 'TestData IPlayer import') -and $ok
$ok = (Assert-Contains $td 'private IPlayer player;' 'TestData EntityPlayer swap') -and $ok
$ok = (Assert-Contains $td 'public IItemStack getItem()' 'TestData ItemStack swap') -and $ok
# Verify old MC imports are gone
$ok = (Assert-NotContains $td 'import net.minecraft.entity.player.EntityPlayer;' 'TestData old EntityPlayer import') -and $ok
$ok = (Assert-NotContains $td 'import net.minecraft.item.ItemStack;' 'TestData old ItemStack import') -and $ok
# Verify no mangled imports (the bug we fixed)
$ok = (Assert-NotContains $tc 'import net.minecraft.nbt.INbt;' 'No mangled import') -and $ok
if ($ok) { $passed++; Write-Host " PASS: Symbol-Swap" -ForegroundColor Green } else { $failed++ }
# ============================================================
Write-Host "`n========== TEST 2: Nuke-Imports ==========" -ForegroundColor Cyan
Reset-Sandbox
$result = Nuke-Imports -Directory $sandboxRel -ForbiddenPrefixes @(
'net.minecraft.', 'cpw.mods.', 'net.minecraftforge.'
)
$tc = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\TestController.java")
$td = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\data\TestData.java")
$ok = $true
$ok = (Assert-NotContains $tc 'import net.minecraft' 'TestController MC imports nuked') -and $ok
$ok = (Assert-NotContains $tc 'import cpw.mods' 'TestController FML imports nuked') -and $ok
$ok = (Assert-NotContains $tc 'import net.minecraftforge' 'TestController Forge imports nuked') -and $ok
$ok = (Assert-Contains $tc 'import noppes.npcs.controllers.data.TestData;' 'TestController project imports preserved') -and $ok
$ok = (Assert-Contains $tc 'import java.util.HashMap;' 'TestController java imports preserved') -and $ok
$ok = (Assert-NotContains $td 'import net.minecraft' 'TestData MC imports nuked') -and $ok
$ok = (Assert-Contains $td 'import noppes.npcs.controllers.TestController;' 'TestData project imports preserved') -and $ok
if ($ok) { $passed++; Write-Host " PASS: Nuke-Imports" -ForegroundColor Green } else { $failed++ }
# ============================================================
Write-Host "`n========== TEST 3: Static-Transform ==========" -ForegroundColor Cyan
Reset-Sandbox
$result = Static-Transform -Directory $sandboxRel -Transforms @(
@('CompressedStreamTools\.writeCompressed\s*\(([^,]+),\s*([^)]+)\)', 'PlatformServiceHolder.get().writeCompressedNBT($1, $2)'),
@('CompressedStreamTools\.readCompressed\s*\(([^)]+)\)', 'PlatformServiceHolder.get().readCompressedNBT($1)'),
@('new\s+NBTTagCompound\s*\(\)', 'NBT.compound()'),
@('new\s+NBTTagList\s*\(\)', 'NBT.list()'),
@('MathHelper\.clamp_int\s*\(', 'ValueUtil.clampInt(')
)
$tc = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\TestController.java")
$td = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\data\TestData.java")
$ok = $true
$ok = (Assert-Contains $tc 'NBT.compound()' 'new NBTTagCompound -> NBT.compound()') -and $ok
$ok = (Assert-Contains $tc 'PlatformServiceHolder.get().writeCompressedNBT(compound, outputStream)' 'writeCompressed transformed') -and $ok
$ok = (Assert-Contains $tc 'PlatformServiceHolder.get().readCompressedNBT(inputStream)' 'readCompressed transformed') -and $ok
$ok = (Assert-Contains $tc 'ValueUtil.clampInt(value, 0, 100)' 'MathHelper.clamp_int transformed') -and $ok
$ok = (Assert-Contains $td 'NBT.list()' 'new NBTTagList -> NBT.list()') -and $ok
if ($ok) { $passed++; Write-Host " PASS: Static-Transform" -ForegroundColor Green } else { $failed++ }
# ============================================================
Write-Host "`n========== TEST 4: Dedupe-Imports ==========" -ForegroundColor Cyan
Reset-Sandbox
$result = Dedupe-Imports -Directory $sandboxRel
$tu = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\util\TestUtil.java")
$ok = $true
# Count how many times 'import noppes.npcs.controllers.TestController;' appears
$dupeCount = ([regex]::Matches($tu, 'import noppes\.npcs\.controllers\.TestController;')).Count
if ($dupeCount -eq 1) {
$ok = $true
} else {
Write-Host " FAIL: Expected 1 TestController import, got $dupeCount" -ForegroundColor Red
$ok = $false
}
$nbtDupeCount = ([regex]::Matches($tu, 'import net\.minecraft\.nbt\.NBTTagCompound;')).Count
if ($nbtDupeCount -eq 1) {
# ok
} else {
Write-Host " FAIL: Expected 1 NBTTagCompound import, got $nbtDupeCount" -ForegroundColor Red
$ok = $false
}
$ok = (Assert-Contains $tu 'import java.util.Map;' 'TestUtil java import preserved') -and $ok
if ($ok) { $passed++; Write-Host " PASS: Dedupe-Imports" -ForegroundColor Green } else { $failed++ }
# ============================================================
Write-Host "`n========== TEST 5: Generate-Stubs ==========" -ForegroundColor Cyan
Reset-Sandbox
# Clean up any previous stub
$stubDir = Join-Path $sandboxAbs 'noppes\npcs\controllers'
$stubFile = Join-Path $stubDir 'SyncController.java'
if (Test-Path $stubFile) { Remove-Item $stubFile -Force }
$result = Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' public static void updateDialog(Object d) {}' }
) -TargetRoot 'tools/migration/test_sandbox/src'
$ok = $true
if (Test-Path $stubFile) {
$stubContent = [IO.File]::ReadAllText($stubFile)
$ok = (Assert-Contains $stubContent 'TEMPORARY STUB' 'Stub has marker comment') -and $ok
$ok = (Assert-Contains $stubContent 'package noppes.npcs.controllers;' 'Stub correct package') -and $ok
$ok = (Assert-Contains $stubContent 'public class SyncController' 'Stub correct class') -and $ok
$ok = (Assert-Contains $stubContent 'public static void updateDialog(Object d) {}' 'Stub has method') -and $ok
} else {
Write-Host " FAIL: Stub file not created" -ForegroundColor Red
$ok = $false
}
# Test that it does NOT overwrite a real class
$realFile = Join-Path $stubDir 'TestController.java'
$realBefore = [IO.File]::ReadAllText($realFile)
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs.controllers'; name='TestController'; type='class'; body='// should not appear' }
) -TargetRoot 'tools/migration/test_sandbox/src' | Out-Null
$realAfter = [IO.File]::ReadAllText($realFile)
if ($realBefore -eq $realAfter) {
# good - real class not overwritten
} else {
Write-Host " FAIL: Generate-Stubs overwrote a real class!" -ForegroundColor Red
$ok = $false
}
# Clean up stub
if (Test-Path $stubFile) { Remove-Item $stubFile -Force }
if ($ok) { $passed++; Write-Host " PASS: Generate-Stubs" -ForegroundColor Green } else { $failed++ }
# ============================================================
Write-Host "`n========== TEST 6: Create-And-Propagate ==========" -ForegroundColor Cyan
Reset-Sandbox
$interfacePath = 'tools/migration/test_sandbox/src/noppes/npcs/api/ITestService.java'
$interfaceContent = @'
package noppes.npcs.api;
public interface ITestService {
void doSomething();
}
'@
$result = Create-And-Propagate `
-FilePath $interfacePath `
-Content $interfaceContent `
-TargetDirectory $sandboxRel `
-Swaps @(
,@('CompressedStreamTools', 'ITestService', 'net.minecraft.nbt.CompressedStreamTools', 'noppes.npcs.api.ITestService')
)
$ok = $true
# Check interface was created
$ifaceFull = Join-Path $PSScriptRoot 'test_sandbox\src\noppes\npcs\api\ITestService.java'
if (Test-Path $ifaceFull) {
$ifaceContent = [IO.File]::ReadAllText($ifaceFull)
$ok = (Assert-Contains $ifaceContent 'public interface ITestService' 'Interface created') -and $ok
} else {
Write-Host " FAIL: Interface file not created" -ForegroundColor Red
$ok = $false
}
# Check propagation in TestController (had CompressedStreamTools references)
$tc = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\TestController.java")
$ok = (Assert-Contains $tc 'ITestService' 'CompressedStreamTools replaced with ITestService') -and $ok
$ok = (Assert-NotContains $tc 'CompressedStreamTools' 'No leftover CompressedStreamTools') -and $ok
$ok = (Assert-Contains $tc 'import noppes.npcs.api.ITestService;' 'New import added') -and $ok
# Clean up
if (Test-Path $ifaceFull) { Remove-Item $ifaceFull -Force }
$apiDir = Join-Path $PSScriptRoot 'test_sandbox\src\noppes\npcs\api'
if (Test-Path $apiDir) { Remove-Item $apiDir -Recurse -Force }
if ($ok) { $passed++; Write-Host " PASS: Create-And-Propagate" -ForegroundColor Green } else { $failed++ }
# ============================================================
Write-Host "`n========== TEST 7: Full Pipeline (Symbol-Swap + Nuke + Static + Dedupe) ==========" -ForegroundColor Cyan
Reset-Sandbox
# Step 1: Nuke MC imports
Nuke-Imports -Directory $sandboxRel -ForbiddenPrefixes @('net.minecraft.', 'cpw.mods.', 'net.minecraftforge.') | Out-Null
# Step 2: Symbol swap whatever is left
Symbol-Swap -Directory $sandboxRel -Swaps @(
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.nbt.INbt'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.nbt.INbtList'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer')
) | Out-Null
# Step 3: Static transforms
Static-Transform -Directory $sandboxRel -Transforms @(
@('new\s+INbt\s*\(\)', 'NBT.compound()'),
@('new\s+INbtList\s*\(\)', 'NBT.list()'),
@('MathHelper\.clamp_int\s*\(', 'ValueUtil.clampInt(')
) | Out-Null
# Step 4: Dedupe
Dedupe-Imports -Directory $sandboxRel | Out-Null
$tc = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\controllers\TestController.java")
$tu = [IO.File]::ReadAllText("$sandboxAbs\noppes\npcs\util\TestUtil.java")
$ok = $true
$ok = (Assert-NotContains $tc 'net.minecraft' 'Pipeline: no MC imports left') -and $ok
$ok = (Assert-NotContains $tc 'cpw.mods' 'Pipeline: no FML imports left') -and $ok
$ok = (Assert-Contains $tc 'INbt' 'Pipeline: symbols swapped') -and $ok
$ok = (Assert-Contains $tc 'NBT.compound()' 'Pipeline: static transforms applied') -and $ok
$ok = (Assert-Contains $tc 'ValueUtil.clampInt(' 'Pipeline: MathHelper transformed') -and $ok
# Check deduplication worked on TestUtil
$tuDupeCount = ([regex]::Matches($tu, 'import noppes\.npcs\.controllers\.TestController;')).Count
if ($tuDupeCount -eq 1) {
# good
} else {
Write-Host " FAIL: Pipeline dedup - expected 1 TestController import, got $tuDupeCount" -ForegroundColor Red
$ok = $false
}
if ($ok) { $passed++; Write-Host " PASS: Full Pipeline" -ForegroundColor Green } else { $failed++ }
# ============================================================
# Clean up sandbox
Reset-Sandbox
# ===== SUMMARY =====
Write-Host "`n============================================" -ForegroundColor White
Write-Host " TEST RESULTS" -ForegroundColor White
Write-Host "============================================" -ForegroundColor White
Write-Host " Passed: $passed" -ForegroundColor Green
Write-Host " Failed: $failed" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'Green' })
Write-Host "============================================" -ForegroundColor White
if ($failed -gt 0) { exit 1 }