28 KiB
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:
- Agent manually writes an
IFoointerface in platform-api - Agent manually writes a
FooWrapperin mc1710 that delegates to the MC class - Agent runs
Create-And-Propagateto 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:
{
"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?"
Scan-Usage -Type "EntityPlayer" -SearchDir "core/src/main/java"
Output: JSON report
{
"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.
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
{
"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.
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):
$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.
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):
$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:
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):
- If method name is in
$mappingswithdelegate→ use that MC method name - If method name is in
$mappingswithtransform→ use verbatim, substituting{{mc}}for field name - If method name is in
$mappingswithbody→ paste the body verbatim - If method name is in
$mappingswithtodo→ generate TODO + throw - 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?"
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.
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
{
"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-Mapuses it to categorize deps as abstracted/not-abstractedGenerate-WrapperuseswrapMethodto auto-generate type conversions in delegationSymbol-Swapcan auto-generate-Swapsfrom the registryBatch-Planknows 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
# 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
# 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
# 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, useScriptXxxnaming (consistent with existing) - B) New
mc1710/.../noppes/npcs/wrapper/package, useXxxWrappernaming (cleaner) - C) Agent decides per-case
Q2: How deep should auto-delegation go?
For IItemStack getHeldItem(), the wrapper needs:
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:
$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:
- Interface exists (copied from API or generated) → source of truth
- Type registry maps MC types to PA types → auto-conversion
- Generate-Wrapper reads interface + registry + optional mappings → wrapper skeleton
- Agent reviews, fills TODOs, fixes edge cases → production code
- Existing tools propagate → done