mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-11 23:07:37 +00:00
VITAL TOOLCHAIN CHAIN FOR MIGRATION (SUPERPOWERS)
This commit is contained in:
1 parent
ea8235320e
commit
f3f800e166
24 files changed
+95310
-18
No files matched your search
@@ -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
@@ -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
@@ -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 }
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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
@@ -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)
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user