FINALIZED ALL MIGRATION TOOLS NEEDED BEFORE EXECUTION

This commit is contained in:
bigguy345 committed 2026-03-16 03:25:23 +02:00
1 parent e26f8a4a01
commit f4aa04dcb6
8 files changed
+8800

No files matched your search

+871
View File
@@ -0,0 +1,871 @@
# MIGRATION EXECUTION PLAN — Core Extraction Wave 1
> **Created:** 2026-03-16
> **Status:** PLANNING (not yet executing)
> **Scope:** Whitelisted packages → core/ via the migration toolchain
> **Data Sources:** `mc_type_classifications_original.json` (37 curated types), `all_methods_export.json` (member data), `mc_usage_index.json` (usage index)
---
## Executive Summary
This plan extracts ~250 files from 8 whitelisted mc1710 packages into `core/`, replacing all MC/Forge type references with platform-api abstractions. It is the **first full-scale use of the migration toolchain** and is structured into 4 sequential phases:
| Phase | What | Tool | Output |
|-------|------|------|--------|
| **1** | Create PA abstractions for all MC types used in scope | `pa_batch.py` | ~20 new PA files in `platform-api/` |
| **2** | Copy whitelisted files to core/ | `Copy-Recursive.ps1` | ~150 new files in `core/` |
| **3** | Replace MC types with PA abstractions in core/ | `Symbol-Swap`, `Static-Transform`, `Nuke-Imports` | All MC imports eliminated from core/ |
| **4** | Verify, diagnose, stabilize | `Build-Report`, `migration-diff` | Clean `:core:compileJava` build |
**Estimated new tool development before execution:** 1 new Python script (`generate_pa_manifest.py`)
---
## 0. Current State Snapshot
### Already Migrated (DO NOT re-copy)
| Location | Count | Contents |
|----------|-------|----------|
| `platform-api/` | 239 files | Entity interfaces (IPlayer, IEntity, etc.), handlers, events, abilities, NBT, overlays, GUI, roles, scoreboard |
| `core/` | 141 files | 8 controllers (Faction, Transport, Tag, Magic, GlobalData, CategoryManager, APIRegistry, ServerTagMap), 30+ data classes, 40+ enums/constants, 6 utils, network enums |
### Whitelisted Migration Scope (mc1710 → core/)
| Package | Files in mc1710 | Already in core/ | Net New |
|---------|----------------|-----------------|---------|
| `kamkeel/npcs/controllers/` (+ data/) | 89 | ~14 (ability data/enums/profile/telegraph) | ~75 |
| `noppes/npcs/controllers/` (+ data/) | 98 | 38 (8 controllers + 30 data classes) | ~60 |
| `noppes/npcs/quests/` | 6 | 0 | 6 |
| `noppes/npcs/roles/` | 24 | 0 | 24 |
| `noppes/npcs/constants/` | 8 | 40 (most already migrated) | ~3 |
| `noppes/npcs/util/` | 10 (3 MC-free) | 6 | ~4 |
| `noppes/npcs/config/` | 15 | 0 | 15 |
| `kamkeel/npcs/network/` | 100+ | 9 (enums only) | **0** (all MC-dependent, skip) |
| **TOTAL** | **~350** | **~107** | **~187** |
### What's NOT in scope (explicitly excluded)
- `containers/`, `guis/`, `gui/`, `fx/` — Client GUI code
- `key/`, `model/`, `renderer/` — Client rendering
- Proxies, Event Handlers — Platform-specific lifecycle
- `kamkeel/npcs/network/` packets — All MC-dependent (100+ files)
- Ability type classes (extend MC Entity) — Stay in mc1710
- Script system — Deeply coupled, not planned
---
## 1. MC Type Classification Registry
### Authoritative Source: `mc_type_classifications_original.json`
This is the **hand-curated** classification of all 400 MC types. The auto-generated `classification_results.json` has inflated counts (46 INTERFACE vs 15, 95 SERVICE vs 12) because it lacks human judgment. **Always use the original.**
| Category | Count | Action |
|----------|-------|--------|
| EXISTING | 11 | Symbol-Swap ready — PA interfaces exist |
| INTERFACE | 15 | Need new PA interface files |
| SERVICE | 12 | Need PlatformService method additions |
| TRANSFORM | 10 | Need Static-Transform regex + support classes |
| SUPPRESS | 352 | Never abstract — code stays in mc1710 |
### 1A. EXISTING Types (11) — Symbol-Swap Ready
These already have PA abstractions. Just run Symbol-Swap.
| MC Type | PA Type | PA Package | Imports |
|---------|---------|------------|---------|
| `NBTTagCompound` | `INbt` | `noppes.npcs.api` | 593 |
| `EntityPlayer` | `IPlayer` | `noppes.npcs.api.entity` | 551 |
| `EntityPlayerMP` | `IPlayer` | `noppes.npcs.api.entity` | 304 |
| `Entity` | `IEntity` | `noppes.npcs.api.entity` | 262 |
| `ItemStack` | `IItemStack` | `noppes.npcs.api.item` | 228 |
| `EntityLivingBase` | `IEntityLivingBase` | `noppes.npcs.api.entity` | 201 |
| `World` | `IWorld` | `noppes.npcs.api` | 172 |
| `NBTTagList` | `INbtList` | `noppes.npcs.api` | 111 |
| `DamageSource` | `IDamageSource` | `noppes.npcs.api` | 36 |
| `WorldServer` | `IWorld` | `noppes.npcs.api` | 18 |
| `EntityLiving` | `IEntityLiving` | `noppes.npcs.api.entity` | 18 |
### 1B. INTERFACE Types (15) — Need PA Creation
New PA interfaces to create in `platform-api/`. Package rule: `net.minecraft.X` → `common.minecraft.X`, `net.minecraftforge.X` → `common.minecraftforge.X`, `cpw.mods.X` → `common.minecraftforge.X`.
| MC Type | PA Name | New Package | Imports | Priority for Scope |
|---------|---------|-------------|---------|-------------------|
| `ResourceLocation` | `IResourceLocation` | `common.minecraft.util` | 145 | **HIGH** — controllers, config |
| `ICommandSender` | `ICommandSender` | `common.minecraft.command` | 47 | **HIGH** — controllers |
| `ChatComponentText` | `ITextComponent` | `common.minecraft.util` | 20 | **HIGH** — controllers (messages) |
| `ChatComponentTranslation` | `ITextComponent` | `common.minecraft.util` | 24 | **HIGH** — controllers (messages) |
| `IChatComponent` | `ITextComponent` | `common.minecraft.util` | 10 | **HIGH** — base type |
| `NBTBase` | `INbtBase` | `common.minecraft.nbt` | 13 | **HIGH** — data classes |
| `Configuration` | `IConfiguration` | `common.minecraftforge.common.config` | 9 | **HIGH** — config system |
| `Property` | `IConfigProperty` | `common.minecraftforge.common.config` | 7 | **HIGH** — config system |
| `AxisAlignedBB` | `IBoundingBox` | `common.minecraft.util` | 59 | **MEDIUM** — some controllers |
| `Vec3` | `IVector3` | `common.minecraft.util` | 43 | **MEDIUM** — some controllers |
| `MovingObjectPosition` | `IRayTraceResult` | `common.minecraft.util` | 15 | **LOW** — mostly abilities |
| `ChatStyle` | `ITextStyle` | `common.minecraft.util` | 3 | **LOW** — text formatting |
| `Potion` | `IPotionType` | `common.minecraft.potion` | 7 | **MEDIUM** — effects system |
| `PotionEffect` | `IPotionEffect` | `common.minecraft.potion` | 6 | **MEDIUM** — effects system |
| `Explosion` | `IExplosion` | `common.minecraft.world` | 3 | **LOW** — abilities only |
**Filtering required**: Run `type-usage <Type> --scope` on each to confirm they're actually used in the whitelisted packages before creating PAs. Types with LOW scope relevance may be deferred.
### 1C. SERVICE Types (12) — PlatformService Method Additions
These are static utilities. Their methods get added to `PlatformService` (or `NBTIO`). No separate files needed — just interface method additions.
| MC Type | Routes To | Imports | Key Methods | Scope Relevance |
|---------|-----------|---------|-------------|-----------------|
| `StatCollector` | `PlatformService` | 106 | `translateToLocal()`, `translateToLocalFormatted()` | **HIGH** |
| `Item` | `PlatformService` | 72 | `getItemById()`, `getIdFromItem()` | **MEDIUM** |
| `MinecraftServer` | `PlatformService` | 26 | `getServer()`, `isDedicatedServer()`, `worldServerForDimension()` | **HIGH** |
| `EntityList` | `PlatformService` | 25 | `getEntityString()`, `createEntityByName()` | **MEDIUM** |
| `FMLCommonHandler` | `PlatformService` | 25 | `getSide()`, `getEffectiveSide()` | **HIGH** |
| `CompressedStreamTools` | `NBTIO` | 24 | `readCompressed()`, `writeCompressed()` | **HIGH** |
| `CommandBase` | `PlatformService` | 14 | `parseDouble()`, `parseInt()` | **LOW** |
| `Loader` | `PlatformService` | 11 | `isModLoaded()` | **HIGH** |
| `I18n` | `PlatformService` | 9 | `format()` | **LOW** (client) |
| `DimensionManager` | `PlatformService` | 2 | `getWorld()` | **LOW** |
| `PlayerSelector` | `PlatformService` | 2 | `matchPlayers()` | **LOW** |
| `NBTUtil` | `NBTIO` | 1 | GameProfile NBT | **LOW** |
### 1D. TRANSFORM Types (10) — Static-Transform Patterns
These get handled by regex replacement. Some need support classes created in `core/`.
| MC Type | Transform Target | Imports | Support Class Needed |
|---------|-----------------|---------|---------------------|
| `Side` + `SideOnly` | `@ClientOnly`/`@ServerOnly` | 800 | Already exist in platform-api |
| `MathHelper` | `ValueUtil.*` / `Math.*` | 98 | `ValueUtil` already in core/ |
| `EnumChatFormatting` | `TextFormatting.*` | 19 | **YES** — `TextFormatting` enum in core/ |
| `Constants` (Forge NBT) | `NbtConstants.*` | 9 | **YES** — `NbtConstants` class in core/ |
| `CommandException` | Same name, new package | 31 | **YES** — `CommandException` in platform-api |
| `SharedMonsterAttributes` | `Attributes.*` | 10 | **YES** — `Attributes` constants in core/ |
| `EnumFacing` | `Direction.*` | 3 | **YES** — `Direction` enum in core/ |
| `ChunkCoordinates` | `BlockPos`/inline | 4 | Uses existing `IPos` |
| `EnumAction` | `ItemAction.*` | 11 | **YES** — `ItemAction` enum in core/ |
**Support classes to create (6):** TextFormatting, NbtConstants, CommandException, Attributes, Direction, ItemAction
---
## 2. Phase 1 — PA Abstraction Creation
### Goal
Create ALL required PA interfaces and support classes BEFORE copying any files. This ensures Symbol-Swap has targets.
### Step 1.1: Scope Verification (NEW TOOL)
Before creating PAs, verify which INTERFACE/SERVICE types are actually used in the whitelisted packages. This prevents creating unused abstractions.
```bash
# For each INTERFACE type, check if it's used in scope
python tools/migration/analyze.py type-usage ResourceLocation --scope noppes/npcs/controllers/ --json
python tools/migration/analyze.py type-usage ResourceLocation --scope noppes/npcs/config/ --json
python tools/migration/analyze.py type-usage ResourceLocation --scope noppes/npcs/roles/ --json
# ... repeat for all types × all packages
```
**⚠️ TOOLING GAP:** This is manual and slow. We need `generate_pa_manifest.py` (see Section 6).
### Step 1.2: Create INTERFACE PA Files
Single `pa_batch.py` manifest with ALL INTERFACE PA files. Package mapping:
- `net.minecraft.*` → `common.minecraft.*`
- `net.minecraftforge.*` → `common.minecraftforge.*`
- `cpw.mods.*` → `common.minecraftforge.*`
**Root:** `platform-api/src/main/java`
**Manifest structure (example entries):**
```json
{
"root": "platform-api/src/main/java",
"files": [
{
"content": "package common.minecraft.util;\n\npublic interface IResourceLocation {\n String getResourceDomain();\n String getResourcePath();\n String toString();\n}\n"
},
{
"content": "package common.minecraft.util;\n\npublic interface ITextComponent {\n String getText();\n String getFormattedText();\n String getUnformattedText();\n ITextComponent appendSibling(ITextComponent sibling);\n ITextComponent setChatStyle(ITextStyle style);\n}\n"
},
{
"content": "package common.minecraft.nbt;\n\npublic interface INbtBase {\n byte getId();\n INbtBase copy();\n}\n"
},
{
"content": "package common.minecraftforge.common.config;\n\npublic interface IConfiguration {\n IConfigProperty get(String category, String key, String defaultValue, String comment);\n IConfigProperty get(String category, String key, int defaultValue, String comment);\n IConfigProperty get(String category, String key, boolean defaultValue, String comment);\n IConfigProperty get(String category, String key, double defaultValue, String comment);\n void save();\n void load();\n boolean hasKey(String category, String key);\n boolean hasCategory(String category);\n}\n"
}
]
}
```
**Method content source:** `all_methods_export.json` — this file has EVERY method/field/static for each MC type with usage counts. The manifest generator reads this to include only methods with usage > 0.
### Step 1.3: Add SERVICE Methods to PlatformService
For SERVICE types, we add methods to the existing `PlatformService` interface. Use `pa_editor.py` to insert method declarations.
**Methods to add to `kamkeel/npcs/platform/PlatformService.java`:**
```java
// StatCollector (106 imports)
String translateToLocal(String key);
String translateToLocalFormatted(String key, Object... args);
// MinecraftServer (26 imports)
boolean isDedicatedServer(); // may already exist
Object getServer();
IWorld getWorldForDimension(int dimensionId);
// FMLCommonHandler (25 imports)
boolean isPhysicalClient(); // already exists
boolean isLogicalClient();
// Loader (11 imports)
boolean isModLoaded(String modId);
// Item registry (72 imports)
IItemStack getItemById(int id);
int getIdFromItem(IItemStack item);
// EntityList (25 imports)
String getEntityRegistryName(IEntity entity);
IEntity createEntityByName(String name, IWorld world);
```
**Methods to add to `noppes/npcs/platform/nbt/NBTIO.java`:**
```java
// CompressedStreamTools (24 imports) — may already exist
INbt readCompressed(java.io.InputStream stream);
void writeCompressed(INbt nbt, java.io.OutputStream stream);
```
### Step 1.4: Create TRANSFORM Support Classes
These go into `core/src/main/java/` (they're platform-independent).
```json
{
"root": "core/src/main/java",
"files": [
{
"content": "package noppes.npcs.constants;\n\npublic class TextFormatting {\n public static final String BLACK = \"\\u00a70\";\n public static final String DARK_BLUE = \"\\u00a71\";\n public static final String DARK_GREEN = \"\\u00a72\";\n public static final String DARK_AQUA = \"\\u00a73\";\n public static final String DARK_RED = \"\\u00a74\";\n public static final String DARK_PURPLE = \"\\u00a75\";\n public static final String GOLD = \"\\u00a76\";\n public static final String GRAY = \"\\u00a77\";\n public static final String DARK_GRAY = \"\\u00a78\";\n public static final String BLUE = \"\\u00a79\";\n public static final String GREEN = \"\\u00a7a\";\n public static final String AQUA = \"\\u00a7b\";\n public static final String RED = \"\\u00a7c\";\n public static final String LIGHT_PURPLE = \"\\u00a7d\";\n public static final String YELLOW = \"\\u00a7e\";\n public static final String WHITE = \"\\u00a7f\";\n public static final String OBFUSCATED = \"\\u00a7k\";\n public static final String BOLD = \"\\u00a7l\";\n public static final String STRIKETHROUGH = \"\\u00a7m\";\n public static final String UNDERLINE = \"\\u00a7n\";\n public static final String ITALIC = \"\\u00a7o\";\n public static final String RESET = \"\\u00a7r\";\n}\n"
},
{
"content": "package noppes.npcs.constants;\n\npublic class NbtConstants {\n public static final int TAG_END = 0;\n public static final int TAG_BYTE = 1;\n public static final int TAG_SHORT = 2;\n public static final int TAG_INT = 3;\n public static final int TAG_LONG = 4;\n public static final int TAG_FLOAT = 5;\n public static final int TAG_DOUBLE = 6;\n public static final int TAG_BYTE_ARRAY = 7;\n public static final int TAG_STRING = 8;\n public static final int TAG_LIST = 9;\n public static final int TAG_COMPOUND = 10;\n public static final int TAG_INT_ARRAY = 11;\n}\n"
}
]
}
```
Additional support classes: `CommandException`, `Attributes`, `Direction`, `ItemAction` — all simple enums/classes with no MC dependencies.
### Step 1.5: Execution Sequence
```
1. Write PA INTERFACE manifest → pa_batch.py --dry-run → pa_batch.py --force
2. Edit PlatformService with SERVICE methods → pa_editor.py --force
3. Write TRANSFORM support class manifest → pa_batch.py --force
4. Verify: ls platform-api/src/main/java/common/ — confirm structure
```
---
## 3. Phase 2 — Core File Copy
### Goal
Copy all files from whitelisted mc1710 packages to core/, following import dependencies within scope.
### Step 2.1: Build the Sources List
Every `.java` file in the whitelisted packages that is NOT already in core/.
**Initial Sources** (explicit files to copy — the roots of the recursion):
```powershell
# noppes/npcs/controllers/ — files NOT already in core/
$controllerSources = @(
'noppes/npcs/controllers/QuestController.java',
'noppes/npcs/controllers/PlayerQuestController.java',
'noppes/npcs/controllers/PlayerDataController.java',
'noppes/npcs/controllers/DialogController.java',
'noppes/npcs/controllers/LinkedItemController.java',
'noppes/npcs/controllers/LinkedNpcController.java',
'noppes/npcs/controllers/PartyController.java',
'noppes/npcs/controllers/AuctionController.java',
'noppes/npcs/controllers/CustomEffectController.java',
'noppes/npcs/controllers/CustomGuiController.java',
'noppes/npcs/controllers/BankController.java',
'noppes/npcs/controllers/ChunkController.java',
'noppes/npcs/controllers/RecipeController.java',
'noppes/npcs/controllers/SpawnController.java',
'noppes/npcs/controllers/ServerCloneController.java',
'noppes/npcs/controllers/ScriptController.java',
'noppes/npcs/controllers/ScriptHookController.java',
'noppes/npcs/controllers/AnimationController.java',
'noppes/npcs/controllers/MarketRegistry.java',
'noppes/npcs/controllers/HookDefinition.java'
# ... + ALL data/* files not already in core/
)
# noppes/npcs/quests/ — all 6 files
$questSources = @(
'noppes/npcs/quests/QuestItem.java',
'noppes/npcs/quests/QuestKill.java',
'noppes/npcs/quests/QuestLocation.java',
'noppes/npcs/quests/QuestDialog.java',
'noppes/npcs/quests/QuestManual.java',
'noppes/npcs/quests/QuestInterface.java'
)
# noppes/npcs/roles/ — all 24 files
$roleSources = @(
'noppes/npcs/roles/RoleTrader.java',
'noppes/npcs/roles/RoleBank.java',
'noppes/npcs/roles/RoleFollower.java',
'noppes/npcs/roles/RoleCompanion.java',
# ... all 24 files
)
# noppes/npcs/config/ — all 15 files
$configSources = @(
'noppes/npcs/config/ConfigMain.java',
'noppes/npcs/config/ConfigClient.java',
'noppes/npcs/config/ConfigScript.java',
'noppes/npcs/config/ConfigMarket.java',
'noppes/npcs/config/ConfigEnergy.java',
'noppes/npcs/config/ConfigItem.java',
'noppes/npcs/config/ConfigDebug.java',
'noppes/npcs/config/ConfigExperimental.java',
'noppes/npcs/config/ConfigMixin.java',
'noppes/npcs/config/LoadConfiguration.java',
'noppes/npcs/config/StringCache.java',
'noppes/npcs/config/GlyphCache.java',
'noppes/npcs/config/legacy/LegacyLoader.java',
'noppes/npcs/config/legacy/LegacyConfig.java',
'noppes/npcs/config/legacy/ConfigProp.java'
)
# MC-free util files only
$utilSources = @(
'noppes/npcs/util/GameProfileAlt.java',
'noppes/npcs/util/JsonException.java',
'noppes/npcs/util/ScriptToStringHelper.java'
)
# kamkeel/npcs/controllers/ — files NOT already in core/
$kamkeelSources = @(
'kamkeel/npcs/controllers/AbilityController.java',
'kamkeel/npcs/controllers/EnergyController.java',
'kamkeel/npcs/controllers/ProfileController.java',
'kamkeel/npcs/controllers/AttributeController.java',
'kamkeel/npcs/controllers/TelegraphController.java',
'kamkeel/npcs/controllers/SyncController.java'
# ... + data/* files not already in core/
)
```
### Step 2.2: AllowPaths & AllowRecursePaths
```powershell
$AllowPaths = @(
'noppes/npcs/controllers/',
'noppes/npcs/quests/',
'noppes/npcs/roles/',
'noppes/npcs/constants/',
'noppes/npcs/util/',
'noppes/npcs/config/',
'kamkeel/npcs/controllers/',
'kamkeel/npcs/util/',
'kamkeel/npcs/config/'
)
# Only recurse into these — prevent explosion into entity/client/network
$AllowRecursePaths = @(
'noppes/npcs/controllers/',
'noppes/npcs/quests/',
'noppes/npcs/roles/',
'noppes/npcs/constants/',
'noppes/npcs/config/',
'kamkeel/npcs/controllers/'
)
# util/ is in AllowPaths but NOT AllowRecursePaths — files get copied but imports aren't chased
```
### Step 2.3: Execute
**IMPORTANT:** `McRoot` defaults to `mc1710/src/main/java` but current code lives at `src/main/java`. Override required.
```powershell
Copy-Recursive `
-Sources ($controllerSources + $questSources + $roleSources + $configSources + $utilSources + $kamkeelSources) `
-AllowPaths $AllowPaths `
-AllowRecursePaths $AllowRecursePaths `
-McRoot 'src/main/java'
```
**Expected output:** ~150-187 files copied to `core/src/main/java/`.
### Step 2.4: What Copy-Recursive Will Skip
Files already in core/ will be reported as EXISTS (not re-copied). Files outside AllowPaths will be reported as SKIPPED. This is correct behavior.
---
## 4. Phase 3 — Symbol Replacement
### Goal
Replace every MC/Forge type reference in core/ with PA abstractions.
### Step 3.1: Nuke-Imports (first)
Remove all MC/Forge/platform-specific imports from core/.
```powershell
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.',
'cpw.mods.',
'net.minecraftforge.',
'noppes.npcs.entity.',
'noppes.npcs.client.',
'noppes.npcs.mixin.',
'kamkeel.npcs.network.',
'kamkeel.npcs.entity.',
'noppes.npcs.containers.',
'noppes.npcs.scripted.',
'noppes.npcs.janino.',
'noppes.npcs.wrapper.',
'noppes.npcs.blocks.',
'noppes.npcs.items.',
'noppes.npcs.ai.'
)
```
### Step 3.2: Symbol-Swap — EXISTING Types (11)
```powershell
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.INbt'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.INbtList'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer'),
@('EntityPlayerMP', 'IPlayer', 'net.minecraft.entity.player.EntityPlayerMP', 'noppes.npcs.api.entity.IPlayer'),
@('Entity', 'IEntity', 'net.minecraft.entity.Entity', 'noppes.npcs.api.entity.IEntity'),
@('ItemStack', 'IItemStack', 'net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack'),
@('EntityLivingBase', 'IEntityLivingBase', 'net.minecraft.entity.EntityLivingBase', 'noppes.npcs.api.entity.IEntityLivingBase'),
@('World', 'IWorld', 'net.minecraft.world.World', 'noppes.npcs.api.IWorld'),
@('WorldServer', 'IWorld', 'net.minecraft.world.WorldServer', 'noppes.npcs.api.IWorld'),
@('DamageSource', 'IDamageSource', 'net.minecraft.util.DamageSource', 'noppes.npcs.api.IDamageSource'),
@('EntityLiving', 'IEntityLiving', 'net.minecraft.entity.EntityLiving', 'noppes.npcs.api.entity.IEntityLiving')
)
```
### Step 3.3: Symbol-Swap — NEW INTERFACE Types
```powershell
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('ResourceLocation', 'IResourceLocation', 'net.minecraft.util.ResourceLocation', 'common.minecraft.util.IResourceLocation'),
@('ICommandSender', 'ICommandSender', 'net.minecraft.command.ICommandSender', 'common.minecraft.command.ICommandSender'),
@('ChatComponentText', 'ITextComponent', 'net.minecraft.util.ChatComponentText', 'common.minecraft.util.ITextComponent'),
@('ChatComponentTranslation', 'ITextComponent', 'net.minecraft.util.ChatComponentTranslation', 'common.minecraft.util.ITextComponent'),
@('IChatComponent', 'ITextComponent', 'net.minecraft.util.IChatComponent', 'common.minecraft.util.ITextComponent'),
@('ChatStyle', 'ITextStyle', 'net.minecraft.util.ChatStyle', 'common.minecraft.util.ITextStyle'),
@('NBTBase', 'INbtBase', 'net.minecraft.nbt.NBTBase', 'common.minecraft.nbt.INbtBase'),
@('Configuration', 'IConfiguration', 'net.minecraftforge.common.config.Configuration', 'common.minecraftforge.common.config.IConfiguration'),
@('Property', 'IConfigProperty', 'net.minecraftforge.common.config.Property', 'common.minecraftforge.common.config.IConfigProperty'),
@('AxisAlignedBB', 'IBoundingBox', 'net.minecraft.util.AxisAlignedBB', 'common.minecraft.util.IBoundingBox'),
@('Vec3', 'IVector3', 'net.minecraft.util.Vec3', 'common.minecraft.util.IVector3'),
@('MovingObjectPosition', 'IRayTraceResult', 'net.minecraft.util.MovingObjectPosition', 'common.minecraft.util.IRayTraceResult'),
@('Potion', 'IPotionType', 'net.minecraft.potion.Potion', 'common.minecraft.potion.IPotionType'),
@('PotionEffect', 'IPotionEffect', 'net.minecraft.potion.PotionEffect', 'common.minecraft.potion.IPotionEffect'),
@('Explosion', 'IExplosion', 'net.minecraft.world.Explosion', 'common.minecraft.world.IExplosion')
)
```
### Step 3.4: Static-Transform — SERVICE Rerouting
```powershell
Static-Transform -Directory 'core/src/main/java' -Transforms @(
# NBT constructors
@('new\s+NBTTagCompound\s*\(\)', 'NBT.compound()'),
@('new\s+NBTTagList\s*\(\)', 'NBT.list()'),
# CompressedStreamTools → NBTIO
@('CompressedStreamTools\.readCompressed\s*\(([^)]+)\)', 'NBTIO.readCompressed($1)'),
@('CompressedStreamTools\.writeCompressed\s*\(([^,]+),\s*([^)]+)\)', 'NBTIO.writeCompressed($1, $2)'),
# StatCollector → PlatformServiceHolder
@('StatCollector\.translateToLocal\s*\(([^)]+)\)', 'PlatformServiceHolder.get().translateToLocal($1)'),
@('StatCollector\.translateToLocalFormatted\s*\(([^)]+)\)', 'PlatformServiceHolder.get().translateToLocalFormatted($1)'),
# MinecraftServer
@('MinecraftServer\.getServer\s*\(\)', 'PlatformServiceHolder.get().getServer()'),
# FMLCommonHandler
@('FMLCommonHandler\.instance\(\)\.getSide\(\)\s*==\s*Side\.CLIENT', 'PlatformServiceHolder.get().isPhysicalClient()'),
@('FMLCommonHandler\.instance\(\)\.getEffectiveSide\(\)\s*==\s*Side\.CLIENT', 'PlatformServiceHolder.get().isLogicalClient()'),
# Loader
@('Loader\.isModLoaded\s*\(([^)]+)\)', 'PlatformServiceHolder.get().isModLoaded($1)'),
# MathHelper
@('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('),
# EnumChatFormatting
@('EnumChatFormatting\.', 'TextFormatting.'),
# Constants.NBT
@('Constants\.NBT\.', 'NbtConstants.'),
# SharedMonsterAttributes
@('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'),
# Annotations
@('@SideOnly\s*\(\s*Side\.CLIENT\s*\)', '@ClientOnly'),
@('@SideOnly\s*\(\s*Side\.SERVER\s*\)', '@ServerOnly')
)
```
### Step 3.5: Generate-Stubs
Classes that will be referenced but aren't in scope. These are TEMPORARY placeholders.
```powershell
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs'; name='CustomNpcs'; type='class';
body=' public static java.io.File Dir = new java.io.File(".");
public static int DefaultInteractLine = 0;
public static java.util.logging.Logger logger;' },
@{ pkg='noppes.npcs'; name='EventHooks'; type='class';
body=' // STUB - event dispatch stays in mc1710' },
@{ pkg='noppes.npcs'; name='LogWriter'; type='class';
body=' public static void info(String msg) {}
public static void warn(String msg) {}
public static void error(String msg) {}
public static void error(String msg, Throwable e) {}' },
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' // STUB - sync stays in mc1710' },
@{ pkg='noppes.npcs.entity'; name='EntityNPCInterface'; type='class';
body=' // STUB - entity stays in mc1710' }
)
```
### Step 3.6: Dedupe-Imports
```powershell
Dedupe-Imports -Directory 'core/src/main/java'
```
### Step 3.7: All-in-One Wave Script
**All steps 3.1–3.6 go into a single `.ps1` file:**
```
tools/migration/wave1_core_extraction.ps1
```
Execute:
```bash
powershell -ExecutionPolicy Bypass -File "tools/migration/wave1_core_extraction.ps1"
```
---
## 5. Phase 4 — Verification & Stabilization
### Goal
Get `:core:compileJava` to pass. Diagnose and fix all errors categorically.
### Step 4.1: Initial Build-Report
```powershell
Build-Report -GradleTask ':core:compileJava' -MaxSamples 10
```
**Expected error categories and fixes:**
| Category | Likely Cause | Fix |
|----------|-------------|-----|
| `MISSING_PACKAGE` | Leftover MC import Nuke-Imports missed | Re-run Nuke with expanded prefixes |
| `MISSING_CLASS` | Transitive dep not in scope | Generate-Stubs |
| `MISSING_METHOD` | PA interface missing method | Add method to PA interface via pa_editor |
| `TYPE_MISMATCH` | Wrong PA type used | Check Symbol-Swap mapping |
| `BAD_ARGS` | Method signature changed | Static-Transform or manual fix |
| `ABSTRACT_VIOLATION` | Class implements MC interface | Remove `implements` or stub |
### Step 4.2: Batch migration-diff
Run `migration-diff` on ALL copied files to verify completeness:
```bash
# For each migrated file:
python tools/migration/analyze.py migration-diff \
--old src/main/java/noppes/npcs/controllers/QuestController.java \
--new core/src/main/java/noppes/npcs/controllers/QuestController.java \
--json
```
**⚠️ TOOLING GAP:** No batch migration-diff exists. Need to script a loop or build `batch_migration_diff.py`.
### Step 4.3: Store Diagnostic Cache
All outputs cached in JSON for easy navigation:
```bash
# Build report
python tools/migration/analyze.py coverage-report --json > tools/migration/_wave1_coverage.json
# All migration-diff results
python tools/migration/_batch_migration_diff.py --output tools/migration/_wave1_diffs.json
```
### Step 4.4: Fix Cycle
```
WHILE build fails:
1. Build-Report → categorize errors
2. MISSING_PACKAGE → Nuke-Imports (add new prefixes)
3. MISSING_CLASS → Generate-Stubs OR Create-And-Propagate (if PA needed)
4. MISSING_METHOD → pa_editor (add to PA) OR Static-Transform (rename)
5. TYPE_MISMATCH → Symbol-Swap (fix mapping) OR manual cast
6. Dedupe-Imports
7. Build-Report → verify error count dropped
```
### Step 4.5: Final Verification
```bash
# Build must pass
./gradlew :core:compileJava
# Coverage should improve significantly
python tools/migration/analyze.py coverage-report --detailed
# mc1710 build must also still pass
./gradlew build
```
---
## 6. Missing Tools — Build Before Execution
### 6A. `generate_pa_manifest.py` (HIGH PRIORITY)
**Purpose:** Bridge the gap between analysis data → executable pa_batch manifest.
**Input:**
- `mc_type_classifications_original.json` — which types need PAs
- `all_methods_export.json` — what methods each type has
- `mc_usage_index.json` — which files use which types
- Package scope filter — list of package prefixes to restrict to
**Output:**
- `pa_batch` manifest JSON with complete Java interface/class content
- Swap table (old → new symbol mappings) for Symbol-Swap
- Static-Transform pattern list for SERVICE types
**Key behavior:**
1. Read classifications, filter to INTERFACE types
2. For each INTERFACE type, filter to types used in specified packages (via usage index)
3. For each filtered type, read methods from `all_methods_export.json`
4. Generate Java interface content with correct package mapping:
- `net.minecraft.X` → `common.minecraft.X`
- `net.minecraftforge.X` → `common.minecraftforge.X`
- `cpw.mods.X` → `common.minecraftforge.X`
5. Output pa_batch manifest JSON
**Estimated development time:** 2-3 hours
**Why this is critical:** Without it, composing the manifest for ~15-20 PA interfaces is manual and error-prone. Each interface needs the RIGHT methods (from all_methods_export), the RIGHT package, and the RIGHT imports. This tool automates the entire process.
### 6B. `batch_migration_diff.py` (MEDIUM PRIORITY)
**Purpose:** Run `migration-diff` on ALL migrated files in one command, output unified JSON.
**Input:**
- Source root (mc1710 path)
- Target root (core path)
- List of migrated file paths (or auto-discover by comparing directories)
**Output:**
- JSON with per-file PASS/WARN/FAIL verdicts
- Aggregate statistics
- Cached for agent navigation
**Estimated development time:** 1-2 hours
### 6C. `generate_wave_script.py` (LOW PRIORITY — nice to have)
**Purpose:** Auto-generate the complete `.ps1` wave script from classification data.
**Input:**
- Type classifications
- Package scope
- Phase parameters
**Output:**
- Complete `.ps1` file with Copy-Recursive + Nuke + Swap + Transform + Stubs + Dedupe + Build
**Estimated development time:** 2-3 hours
**Why low priority:** The wave script can be composed manually from the plan. Automation saves time on FUTURE waves but is not blocking for Wave 1.
---
## 7. Execution Checklist
### Pre-Flight
- [ ] **Tool development:** Build `generate_pa_manifest.py`
- [ ] **Scope verification:** Run `type-usage` for each INTERFACE/SERVICE type against whitelisted packages
- [ ] **Confirm existing PAs:** Verify all 11 EXISTING types have complete PA interfaces (run `surface-miner --compare-existing`)
- [ ] **Git clean state:** Ensure working tree is clean before starting
### Phase 1 Execution
- [ ] Generate PA manifest via `generate_pa_manifest.py`
- [ ] Dry-run: `pa_batch.py manifest.json --dry-run`
- [ ] Create PAs: `pa_batch.py manifest.json --force`
- [ ] Edit PlatformService with SERVICE methods: `pa_editor.py`
- [ ] Create TRANSFORM support classes: `pa_batch.py`
- [ ] Verify: `ls platform-api/src/main/java/common/`
### Phase 2 Execution
- [ ] Write wave script with Copy-Recursive params
- [ ] Execute: Copy ~150-187 files to core/
- [ ] Verify: count files in core/ (should increase by ~150)
### Phase 3 Execution
- [ ] Execute wave script (Nuke + Swap + Transform + Stubs + Dedupe)
- [ ] Initial Build-Report
### Phase 4 Execution
- [ ] Fix cycle until `:core:compileJava` passes
- [ ] Run batch migration-diff
- [ ] Store diagnostic cache
- [ ] Run `coverage-report` — measure improvement
- [ ] Verify mc1710 build still passes: `./gradlew build`
- [ ] **Human commits** (agent does NOT commit)
---
## 8. Risk Assessment
| Risk | Impact | Mitigation |
|------|--------|------------|
| Script system files copied (deeply MC-coupled) | Build explosion | AllowRecursePaths prevents recursion into script packages |
| Symbol-Swap replaces too aggressively (e.g., `Entity` → `IEntity` in non-type contexts) | Broken code | Word-boundary regex in Symbol-Swap handles this. Verify with migration-diff. |
| Some controllers have too many MC deps to clean up | Phase 3 stalls | Generate-Stubs for anything not in scope. Fix cycle handles residuals. |
| Config system needs Forge Configuration deeply | Config files can't compile | IConfiguration PA must match Forge API surface closely. Use surface-miner to verify. |
| Role/Quest files reference EntityNPCInterface | Missing stubs | Generate-Stubs for EntityNPCInterface. Core version is a placeholder. |
| Copy-Recursive pulls in too many transitive deps | Scope explosion | AllowPaths + AllowRecursePaths constrain the recursion tightly |
---
## 9. Success Criteria
| Metric | Before | Target |
|--------|--------|--------|
| Files in core/ | 141 | ~290-330 |
| PA type coverage (types with abstractions) | 11/400 (2.8%) | ~30/400 (7.5%) |
| PA import coverage (imports to abstracted types) | 37.1% | ~55-60% |
| `:core:compileJava` | PASS | PASS |
| `:build` (mc1710) | PASS | PASS |
---
## Appendix A: Complete Swap Table
This is the full `-Swaps` parameter for Symbol-Swap, combining EXISTING + new INTERFACE types.
```powershell
$AllSwaps = @(
# === EXISTING (11) ===
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.INbt'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.INbtList'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer'),
@('EntityPlayerMP', 'IPlayer', 'net.minecraft.entity.player.EntityPlayerMP', 'noppes.npcs.api.entity.IPlayer'),
@('Entity', 'IEntity', 'net.minecraft.entity.Entity', 'noppes.npcs.api.entity.IEntity'),
@('ItemStack', 'IItemStack', 'net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack'),
@('EntityLivingBase', 'IEntityLivingBase', 'net.minecraft.entity.EntityLivingBase', 'noppes.npcs.api.entity.IEntityLivingBase'),
@('World', 'IWorld', 'net.minecraft.world.World', 'noppes.npcs.api.IWorld'),
@('WorldServer', 'IWorld', 'net.minecraft.world.WorldServer', 'noppes.npcs.api.IWorld'),
@('DamageSource', 'IDamageSource', 'net.minecraft.util.DamageSource', 'noppes.npcs.api.IDamageSource'),
@('EntityLiving', 'IEntityLiving', 'net.minecraft.entity.EntityLiving', 'noppes.npcs.api.entity.IEntityLiving'),
# === NEW INTERFACE (15) ===
@('ResourceLocation', 'IResourceLocation', 'net.minecraft.util.ResourceLocation', 'common.minecraft.util.IResourceLocation'),
@('ICommandSender', 'ICommandSender', 'net.minecraft.command.ICommandSender', 'common.minecraft.command.ICommandSender'),
@('ChatComponentText', 'ITextComponent', 'net.minecraft.util.ChatComponentText', 'common.minecraft.util.ITextComponent'),
@('ChatComponentTranslation', 'ITextComponent', 'net.minecraft.util.ChatComponentTranslation', 'common.minecraft.util.ITextComponent'),
@('IChatComponent', 'ITextComponent', 'net.minecraft.util.IChatComponent', 'common.minecraft.util.ITextComponent'),
@('ChatStyle', 'ITextStyle', 'net.minecraft.util.ChatStyle', 'common.minecraft.util.ITextStyle'),
@('NBTBase', 'INbtBase', 'net.minecraft.nbt.NBTBase', 'common.minecraft.nbt.INbtBase'),
@('Configuration', 'IConfiguration', 'net.minecraftforge.common.config.Configuration', 'common.minecraftforge.common.config.IConfiguration'),
@('Property', 'IConfigProperty', 'net.minecraftforge.common.config.Property', 'common.minecraftforge.common.config.IConfigProperty'),
@('AxisAlignedBB', 'IBoundingBox', 'net.minecraft.util.AxisAlignedBB', 'common.minecraft.util.IBoundingBox'),
@('Vec3', 'IVector3', 'net.minecraft.util.Vec3', 'common.minecraft.util.IVector3'),
@('MovingObjectPosition', 'IRayTraceResult', 'net.minecraft.util.MovingObjectPosition', 'common.minecraft.util.IRayTraceResult'),
@('Potion', 'IPotionType', 'net.minecraft.potion.Potion', 'common.minecraft.potion.IPotionType'),
@('PotionEffect', 'IPotionEffect', 'net.minecraft.potion.PotionEffect', 'common.minecraft.potion.IPotionEffect'),
@('Explosion', 'IExplosion', 'net.minecraft.world.Explosion', 'common.minecraft.world.IExplosion')
)
```
## Appendix B: Static-Transform Registry
Complete list from Section 4, Step 3.4. All patterns use .NET regex syntax for PowerShell.
## Appendix C: File Inventory by Package
### Files to copy (not already in core/)
**noppes/npcs/controllers/** (estimated ~60 new):
- QuestController, PlayerQuestController, PlayerDataController, DialogController
- LinkedItemController, LinkedNpcController, PartyController, AuctionController
- CustomEffectController, CustomGuiController, BankController, ChunkController
- RecipeController, SpawnController, ServerCloneController, AnimationController
- ScriptController, ScriptHookController, MarketRegistry, HookDefinition
- + ~40 data/* classes (Quest, Dialog, PlayerData, etc.)
**noppes/npcs/quests/** (6 new):
- QuestItem, QuestKill, QuestLocation, QuestDialog, QuestManual, QuestInterface
**noppes/npcs/roles/** (24 new):
- RoleTrader, RoleBank, RoleFollower, RoleCompanion, RoleTransporter, RolePostman
- RoleMount, RoleInterface, RoleInnkeeper, RoleAuctioneer
- JobBard, JobGuard, JobHealer, JobSpawner, JobItemGiver, JobFollower, JobConversation, JobChunkLoader, JobInterface
- companion/* (5 files)
**noppes/npcs/config/** (15 new):
- ConfigMain, ConfigClient, ConfigScript, ConfigMarket, ConfigEnergy, ConfigItem
- ConfigDebug, ConfigExperimental, ConfigMixin, LoadConfiguration, StringCache, GlyphCache
- legacy/* (3 files)
**noppes/npcs/util/** (3 new MC-free):
- GameProfileAlt, JsonException, ScriptToStringHelper
**kamkeel/npcs/controllers/** (estimated ~75 new):
- AbilityController, EnergyController, ProfileController, AttributeController
- TelegraphController, SyncController
- + ~69 data/* classes (abilities, ability types, conditions, etc.)
**noppes/npcs/constants/** (~3 new):
- ScriptContext, EnumPotionType, EnumNotificationType (others already in core/)
File diff suppressed because it is too large. Load diff
+141
View File
@@ -0,0 +1,141 @@
# ============================================================
# Wave Migration Script
# Generated by generate_wave_script.py on 2026-03-16
# Source: tools/migration/mc_type_mappings.json
# ============================================================
. (Join-Path $PSScriptRoot 'Copy-Recursive.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Static-Transform.ps1')
. (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# ============================================================
# STEP 2: Nuke MC imports
# ============================================================
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.',
'cpw.mods.',
'net.minecraftforge.',
'noppes.npcs.entity.',
'noppes.npcs.client.',
'noppes.npcs.mixin.',
'kamkeel.npcs.network.',
'kamkeel.npcs.entity.',
'noppes.npcs.containers.',
'noppes.npcs.scripted.',
'noppes.npcs.janino.',
'noppes.npcs.wrapper.',
'noppes.npcs.blocks.',
'noppes.npcs.items.',
'noppes.npcs.ai.'
)
# ============================================================
# STEP 3: Symbol-Swap — replace MC types with PA abstractions
# ============================================================
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.INbt'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer'),
@('EntityPlayerMP', 'IPlayer', 'net.minecraft.entity.player.EntityPlayerMP', 'noppes.npcs.api.entity.IPlayer'),
@('Entity', 'IEntity', 'net.minecraft.entity.Entity', 'noppes.npcs.api.entity.IEntity'),
@('ItemStack', 'IItemStack', 'net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack'),
@('EntityLivingBase', 'IEntityLivingBase', 'net.minecraft.entity.EntityLivingBase', 'noppes.npcs.api.entity.IEntityLivingBase'),
@('World', 'IWorld', 'net.minecraft.world.World', 'noppes.npcs.api.IWorld'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.INbtList'),
@('DamageSource', 'IDamageSource', 'net.minecraft.util.DamageSource', 'noppes.npcs.api.IDamageSource'),
@('WorldServer', 'IWorld', 'net.minecraft.world.WorldServer', 'noppes.npcs.api.IWorld'),
@('EntityLiving', 'IEntityLiving', 'net.minecraft.entity.EntityLiving', 'noppes.npcs.api.entity.IEntityLiving'),
@('ResourceLocation', 'IResourceLocation', 'net.minecraft.util.ResourceLocation', 'noppes.npcs.api.IResourceLocation'),
@('AxisAlignedBB', 'IBoundingBox', 'net.minecraft.util.AxisAlignedBB', 'noppes.npcs.api.IBoundingBox'),
@('ICommandSender', 'ICommandSender', 'net.minecraft.command.ICommandSender', 'noppes.npcs.api.ICommandSender'),
@('Vec3', 'IVector3', 'net.minecraft.util.Vec3', 'noppes.npcs.api.IVector3'),
@('ChatComponentText', 'ITextComponent', 'net.minecraft.util.ChatComponentText', 'noppes.npcs.api.ITextComponent'),
@('ChatComponentTranslation', 'ITextComponent', 'net.minecraft.util.ChatComponentTranslation', 'noppes.npcs.api.ITextComponent'),
@('IChatComponent', 'ITextComponent', 'net.minecraft.util.IChatComponent', 'noppes.npcs.api.ITextComponent'),
@('ChatStyle', 'ITextStyle', 'net.minecraft.util.ChatStyle', 'noppes.npcs.api.ITextStyle'),
@('MovingObjectPosition', 'IRayTraceResult', 'net.minecraft.util.MovingObjectPosition', 'noppes.npcs.api.IRayTraceResult'),
@('NBTBase', 'INbtBase', 'net.minecraft.nbt.NBTBase', 'noppes.npcs.api.INbtBase'),
@('Potion', 'IPotionType', 'net.minecraft.potion.Potion', 'noppes.npcs.api.IPotionType'),
@('PotionEffect', 'IPotionEffect', 'net.minecraft.potion.PotionEffect', 'noppes.npcs.api.IPotionEffect'),
@('Explosion', 'IExplosion', 'net.minecraft.world.Explosion', 'noppes.npcs.api.IExplosion'),
@('Configuration', 'IForgeConfig', 'net.minecraftforge.common.config.Configuration', 'noppes.npcs.api.IForgeConfig'),
@('Property', 'IConfigProperty', 'net.minecraftforge.common.config.Property', 'noppes.npcs.api.IConfigProperty')
)
# ============================================================
# STEP 4: Static-Transform — regex rewrites
# ============================================================
Static-Transform -Directory 'core/src/main/java' -Transforms @(
@('@SideOnly\s*\(\s*Side\.CLIENT\s*\)', '@ClientOnly'),
@('@SideOnly\s*\(\s*Side\.SERVER\s*\)', '@ServerOnly'),
@('Side\.CLIENT', 'PlatformSide.CLIENT'),
@('Side\.SERVER', 'PlatformSide.SERVER'),
@('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('),
@('EnumChatFormatting\.', 'TextFormatting.'),
@('EnumFacing\.', 'Direction.'),
@('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'),
@('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'),
@('net\.minecraft\.command\.CommandException', 'noppes.npcs.api.CommandException'),
@('new CommandException\(', 'new CommandException('),
@('new ChunkCoordinates\(([^,]+),\s*([^,]+),\s*([^)]+)\)', 'new BlockPos($1, $2, $3)'),
@('\.posX\b', '.getX()'),
@('\.posY\b', '.getY()'),
@('\.posZ\b', '.getZ()'),
@('EnumAction\.', 'ItemAction.')
)
# ============================================================
# STEP 5: Generate stubs for missing dependencies
# ============================================================
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs'; name='CustomNpcs'; type='class';
body=' public static java.io.File Dir = new java.io.File(".");
public static int DefaultInteractLine = 0;' },
@{ pkg='noppes.npcs'; name='EventHooks'; type='class';
body=' // STUB - event dispatch stays in mc1710' },
@{ pkg='noppes.npcs'; name='LogWriter'; type='class';
body=' public static void info(String msg) {}
public static void warn(String msg) {}
public static void error(String msg) {}
public static void error(String msg, Throwable e) {}' },
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' // STUB - sync stays in mc1710' }
)
# ============================================================
# STEP 6: Deduplicate imports
# ============================================================
Dedupe-Imports -Directory 'core/src/main/java'
# ============================================================
# STEP 7: Build and report
# ============================================================
Build-Report -GradleTask ':core:compileJava'
+200
View File
@@ -16,6 +16,7 @@ Subcommands:
coverage-report Migration coverage analysis (P3)
classify Classify MC type for migration strategy (P0)
classify-all Classify ALL MC types in index and output as JSON (P0)
generate-mappings Generate mc_type_mappings.json from curated classifications
Usage:
python analyze.py query-type EntityPlayer
@@ -2251,6 +2252,204 @@ def add_migration_diff_args(subparsers):
p.set_defaults(func=cmd_migration_diff)
# ============================================================
# generate-mappings: Build mc_type_mappings.json from curated classifications
# ============================================================
def cmd_generate_mappings(args):
"""Read mc_type_classifications_original.json and produce mc_type_mappings.json."""
project_root = (
Path(args.project_root) if args.project_root else auto_detect_project_root()
)
cls_path = Path(args.classifications)
if not cls_path.is_absolute():
cls_path = project_root / args.classifications
if not cls_path.exists():
print(f"ERROR: Classifications file not found: {cls_path}", file=sys.stderr)
return 1
with open(cls_path, "r", encoding="utf-8") as f:
cls_data = json.load(f)
types = cls_data.get("types", {})
allowed_categories = None
if args.category:
allowed_categories = set(c.upper() for c in args.category)
mappings = {}
category_counts = defaultdict(int)
for type_name, info in types.items():
if type_name.startswith("__"):
continue
category = info.get("category", "")
if not category:
continue
if allowed_categories and category not in allowed_categories:
continue
category_counts[category] += 1
imports = info.get("imports", 0)
fqn = info.get("fqn", "")
if category == "EXISTING":
pa_name = info.get("pa", "")
pa_package = info.get("package", "")
pa_fqn = f"{pa_package}.{pa_name}" if pa_package and pa_name else ""
entry = {
"fqn": fqn,
"category": category,
"pa_name": pa_name,
"pa_package": pa_package,
"pa_fqn": pa_fqn,
"imports": imports,
"swap": [type_name, pa_name, fqn, pa_fqn],
}
elif category == "INTERFACE":
pa_name = info.get("pa", "")
pa_package = info.get("package", "")
pa_fqn = f"{pa_package}.{pa_name}" if pa_package and pa_name else ""
entry = {
"fqn": fqn,
"category": category,
"pa_name": pa_name,
"pa_package": pa_package,
"pa_fqn": pa_fqn,
"imports": imports,
"swap": [type_name, pa_name, fqn, pa_fqn],
}
if "methods" in info:
entry["methods"] = info["methods"]
if "constructors" in info:
entry["constructors"] = info["constructors"]
elif category == "SERVICE":
entry = {
"fqn": fqn,
"category": category,
"service_target": info.get("service", ""),
"imports": imports,
"methods": info.get("methods", {}),
"swap": None,
}
elif category == "TRANSFORM":
entry = {
"fqn": fqn,
"category": category,
"patterns": info.get("patterns", []),
"imports": imports,
"swap": None,
}
elif category == "SUPPRESS":
entry = {
"fqn": fqn,
"category": category,
"reason": info.get("reason", ""),
"imports": imports,
"swap": None,
}
else:
entry = {
"fqn": fqn,
"category": category,
"imports": imports,
"swap": None,
}
mappings[type_name] = entry
timestamp = datetime.now(timezone.utc).isoformat()
total_types = len(mappings)
cat_order = ["EXISTING", "INTERFACE", "SERVICE", "TRANSFORM", "SUPPRESS"]
categories_summary = {}
for cat in cat_order:
if category_counts.get(cat, 0) > 0:
categories_summary[cat] = category_counts[cat]
for cat, count in sorted(category_counts.items()):
if cat not in categories_summary and count > 0:
categories_summary[cat] = count
output = {
"_meta": {
"generated": timestamp,
"source": cls_path.name,
"total_types": total_types,
"categories": categories_summary,
},
"mappings": mappings,
}
if args.json_output:
json.dump(output, sys.stdout, indent=2, ensure_ascii=True)
print()
else:
output_path = Path(args.output)
if not output_path.is_absolute():
output_path = project_root / args.output
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
f.write("\n")
print()
print("=" * 80)
print("GENERATE MAPPINGS")
print("=" * 80)
print(f" Source: {cls_path.name}")
print(f" Total types: {total_types}")
print()
print(" CATEGORY BREAKDOWN:")
for cat in cat_order:
count = categories_summary.get(cat, 0)
if count > 0:
print(f" {cat:<14}{count:>4} types")
print()
print(f" Output written: {output_path}")
print("=" * 80)
print()
return 0
def add_generate_mappings_args(subparsers):
p = subparsers.add_parser(
"generate-mappings",
help="Generate mc_type_mappings.json from curated classifications",
)
p.add_argument(
"--output",
default="tools/migration/mc_type_mappings.json",
help="Output JSON file path (default: tools/migration/mc_type_mappings.json)",
)
p.add_argument(
"--classifications",
default="tools/migration/mc_type_classifications_original.json",
help="Path to source classifications JSON (relative to project root)",
)
p.add_argument(
"--json",
dest="json_output",
action="store_true",
help="Print output to stdout as JSON instead of writing to file",
)
p.add_argument(
"--category",
nargs="+",
help="Filter to specific categories (e.g. EXISTING INTERFACE)",
)
p.add_argument("--project-root", help="Project root (auto-detected if omitted)")
p.set_defaults(func=cmd_generate_mappings)
# ============================================================
# Main CLI
# ============================================================
@@ -2274,6 +2473,7 @@ def main():
add_coverage_report_args(subparsers)
add_classify_args(subparsers)
add_classify_all_args(subparsers)
add_generate_mappings_args(subparsers)
args = parser.parse_args()
+488
View File
@@ -0,0 +1,488 @@
"""
batch_migration_diff.py — Batch Migration Diff
================================================
Runs migration-diff logic on ALL migrated files in core/ and produces
one unified JSON with every file's verdict (PASS/WARN/FAIL/NO_COUNTERPART).
Usage:
python tools/migration/batch_migration_diff.py
python tools/migration/batch_migration_diff.py --json
python tools/migration/batch_migration_diff.py --output tools/migration/_batch_diffs.json
python tools/migration/batch_migration_diff.py --mc-root src/main/java --core-root core/src/main/java
python tools/migration/batch_migration_diff.py --classifications tools/migration/mc_type_classifications_original.json
python tools/migration/batch_migration_diff.py --project-root /path/to/project
"""
import sys
import os
import json
import argparse
import difflib
import re
from pathlib import Path
from datetime import datetime, timezone
# Add script directory to path so we can import analyze.py and pa_rules.py
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
def auto_detect_project_root():
"""Auto-detect project root from script location (tools/migration/ -> root)."""
return SCRIPT_DIR.parent.parent
def _try_import_analyze():
"""Try to import functions from analyze.py. Returns a dict of functions or None."""
try:
from analyze import (
setup_treesitter_parser,
_load_classifications,
_extract_method_bodies,
_normalized_signature_key,
normalize_text,
_scan_unmigrated_types,
)
return {
"setup_treesitter_parser": setup_treesitter_parser,
"_load_classifications": _load_classifications,
"_extract_method_bodies": _extract_method_bodies,
"_normalized_signature_key": _normalized_signature_key,
"normalize_text": normalize_text,
"_scan_unmigrated_types": _scan_unmigrated_types,
}
except ImportError as e:
print(f"WARNING: Could not import from analyze.py: {e}", file=sys.stderr)
return None
def _normalize_classifications(data):
"""Normalize both mc_type_mappings.json and mc_type_classifications_original.json formats."""
types = data.get("types") or data.get("mappings") or {}
normalized = {}
for type_name, info in types.items():
if type_name.startswith("_"):
normalized[type_name] = info
continue
entry = dict(info)
if "pa_name" in entry and "pa" not in entry:
entry["pa"] = entry["pa_name"]
if "pa_package" in entry and "package" not in entry:
entry["package"] = entry["pa_package"]
if "service_name" in entry and "service" not in entry:
entry["service"] = entry["service_name"]
normalized[type_name] = entry
return normalized
def _load_classifications_fallback(project_root, classifications_path):
cls_file = Path(project_root) / classifications_path
if not cls_file.exists():
raise FileNotFoundError(f"Classifications file not found: {cls_file}")
with open(cls_file, "r", encoding="utf-8") as f:
data = json.load(f)
return _normalize_classifications(data)
def _scan_unmigrated_types_fallback(file_text, classifications):
"""Scan file text for MC type names that should have been replaced. Standalone fallback."""
results = []
lines = file_text.split("\n")
for type_name, info in classifications.items():
if type_name.startswith("_"):
continue
category = info.get("category", "")
if category not in ("EXISTING", "INTERFACE", "SERVICE"):
continue
pa_name = info.get("pa") or info.get("service")
if not pa_name:
continue
if len(type_name) < 4:
continue
pattern = re.compile(r"\b" + re.escape(type_name) + r"\b")
found_lines = []
has_import = False
for i, line in enumerate(lines, 1):
if pattern.search(line):
if line.strip().startswith("import "):
has_import = True
found_lines.append(i)
if found_lines:
results.append(
{
"mc_name": type_name,
"pa_name": pa_name,
"lines": found_lines,
"in_import": has_import,
}
)
return results
def _resolve_classifications_path(args):
"""Resolve which classifications file to use."""
if args.classifications:
return args.classifications
# Prefer mc_type_mappings.json if it exists
project_root = (
Path(args.project_root) if args.project_root else auto_detect_project_root()
)
mappings_path = project_root / "tools" / "migration" / "mc_type_mappings.json"
if mappings_path.exists():
return "tools/migration/mc_type_mappings.json"
# Fallback to original classifications
original_path = (
project_root / "tools" / "migration" / "mc_type_classifications_original.json"
)
if original_path.exists():
return "tools/migration/mc_type_classifications_original.json"
# Default
return "tools/migration/mc_type_classifications.json"
def run_single_diff(old_file, new_file, parser, classifications, analyze_fns):
"""
Run migration-diff logic on a single file pair.
Returns a result dict with verdict, unmigrated, missing_methods, semantic_body_diffs.
"""
result = {
"unmigrated": [],
"missing_methods": [],
"semantic_body_diffs": [],
}
# Step 1: Scan for unmigrated types in new file
new_text = new_file.read_text(encoding="utf-8", errors="replace")
if analyze_fns:
unmigrated = analyze_fns["_scan_unmigrated_types"](new_text, classifications)
else:
unmigrated = _scan_unmigrated_types_fallback(new_text, classifications)
result["unmigrated"] = unmigrated
# Step 2 & 3: Method-level analysis (requires tree-sitter)
if parser is not None and analyze_fns:
try:
_extract = analyze_fns["_extract_method_bodies"]
_norm_key = analyze_fns["_normalized_signature_key"]
_norm_text = analyze_fns["normalize_text"]
old_methods, _ = _extract(old_file, parser)
new_methods, _ = _extract(new_file, parser)
# Build maps by normalized key
old_method_map = {}
for m in old_methods:
key = _norm_key(m, classifications)
old_method_map[key] = m
new_method_map = {}
for m in new_methods:
key = _norm_key(m, classifications)
new_method_map[key] = m
old_keys = set(old_method_map.keys())
new_keys = set(new_method_map.keys())
# Missing methods
for key in sorted(old_keys - new_keys):
m = old_method_map[key]
result["missing_methods"].append(
{
"name": m["name"],
"signature": m["signature_text"],
"line": m["line"],
}
)
# Semantic body diffs
common_keys = old_keys & new_keys
for key in sorted(common_keys):
old_m = old_method_map[key]
new_m = new_method_map[key]
old_body = old_m.get("body")
new_body = new_m.get("body")
if old_body is None or new_body is None:
continue
norm_old = _norm_text(old_body, classifications)
norm_new = _norm_text(new_body, classifications)
norm_old_lines = norm_old.strip().splitlines()
norm_new_lines = norm_new.strip().splitlines()
matcher = difflib.SequenceMatcher(None, norm_old_lines, norm_new_lines)
ratio = matcher.ratio()
diff_lines = list(
difflib.unified_diff(
norm_old_lines,
norm_new_lines,
fromfile="old (normalized)",
tofile="new (normalized)",
lineterm="",
)
)
change_lines = [
l
for l in diff_lines
if (l.startswith("+") or l.startswith("-"))
and not l.startswith("+++")
and not l.startswith("---")
]
if ratio < 0.85 or len(change_lines) > 0:
result["semantic_body_diffs"].append(
{
"name": old_m["name"],
"signature": old_m["signature_text"],
"similarity_ratio": round(ratio, 3),
"diff_line_count": len(change_lines),
"diff_preview": diff_lines[:10],
}
)
except Exception as e:
result["note"] = f"tree-sitter analysis error: {str(e)}"
else:
result["note"] = "tree-sitter not available"
# Verdict
if result["missing_methods"]:
result["verdict"] = "FAIL"
result["verdict_reason"] = f"{len(result['missing_methods'])} missing method(s)"
elif result["unmigrated"] or result["semantic_body_diffs"]:
parts = []
if result["unmigrated"]:
parts.append(f"{len(result['unmigrated'])} unmigrated type(s)")
if result["semantic_body_diffs"]:
parts.append(f"{len(result['semantic_body_diffs'])} body diff(s)")
result["verdict"] = "WARN"
result["verdict_reason"] = ", ".join(parts)
else:
result["verdict"] = "PASS"
result["verdict_reason"] = (
"all methods present, no unmigrated types, no body drift"
)
return result
def main():
parser = argparse.ArgumentParser(
description="Batch migration-diff: run migration-diff on ALL core/ files"
)
parser.add_argument(
"--mc-root",
default="src/main/java",
help="MC 1.7.10 source root (default: src/main/java)",
)
parser.add_argument(
"--core-root",
default="core/src/main/java",
help="Core source root (default: core/src/main/java)",
)
parser.add_argument(
"--classifications",
help="Path to classifications JSON (relative to project root)",
)
parser.add_argument(
"--output",
default="tools/migration/_batch_diffs.json",
help="Output JSON file path (relative to project root)",
)
parser.add_argument(
"--json",
dest="json_output",
action="store_true",
help="Print JSON to stdout instead of human-readable output",
)
parser.add_argument(
"--project-root",
help="Project root (auto-detected if omitted)",
)
args = parser.parse_args()
project_root = (
Path(args.project_root) if args.project_root else auto_detect_project_root()
)
# Resolve classifications path
classifications_path = _resolve_classifications_path(args)
# Try to import from analyze.py
analyze_fns = _try_import_analyze()
# Load classifications
if analyze_fns:
try:
classifications = analyze_fns["_load_classifications"](
project_root, classifications_path
)
if not classifications:
classifications = _load_classifications_fallback(
project_root, classifications_path
)
except (FileNotFoundError, KeyError):
classifications = _load_classifications_fallback(
project_root, classifications_path
)
else:
classifications = _load_classifications_fallback(
project_root, classifications_path
)
# Setup tree-sitter parser
ts_parser = None
if analyze_fns:
try:
ts_parser, _ = analyze_fns["setup_treesitter_parser"]()
except Exception:
pass
# Enumerate all .java files in core-root
core_root = project_root / args.core_root
mc_root = project_root / args.mc_root
if not core_root.exists():
print(f"ERROR: Core root not found: {core_root}", file=sys.stderr)
return 1
core_files = sorted(core_root.rglob("*.java"))
total_files = len(core_files)
results = []
summary = {"PASS": [], "WARN": [], "FAIL": [], "NO_COUNTERPART": []}
files_with_counterpart = 0
files_without_counterpart = 0
for core_file in core_files:
rel_path = core_file.relative_to(core_root)
# Normalize to forward slashes for consistency
rel_path_str = str(rel_path).replace("\\", "/")
mc_file = mc_root / rel_path
if mc_file.exists():
files_with_counterpart += 1
diff_result = run_single_diff(
mc_file, core_file, ts_parser, classifications, analyze_fns
)
entry = {
"relative_path": rel_path_str,
"old_file": str(Path(args.mc_root) / rel_path).replace("\\", "/"),
"new_file": str(Path(args.core_root) / rel_path).replace("\\", "/"),
"verdict": diff_result["verdict"],
"verdict_reason": diff_result["verdict_reason"],
"unmigrated_count": len(diff_result["unmigrated"]),
"missing_methods_count": len(diff_result["missing_methods"]),
"semantic_diffs_count": len(diff_result["semantic_body_diffs"]),
"unmigrated": diff_result["unmigrated"],
"missing_methods": diff_result["missing_methods"],
"semantic_body_diffs": diff_result["semantic_body_diffs"],
}
if "note" in diff_result:
entry["note"] = diff_result["note"]
results.append(entry)
summary[diff_result["verdict"]].append(rel_path_str)
else:
files_without_counterpart += 1
entry = {
"relative_path": rel_path_str,
"verdict": "NO_COUNTERPART",
"verdict_reason": "No mc1710 original found",
"new_file": str(Path(args.core_root) / rel_path).replace("\\", "/"),
}
results.append(entry)
summary["NO_COUNTERPART"].append(rel_path_str)
# Build output
output = {
"_meta": {
"generated": datetime.now(timezone.utc).isoformat(),
"mc_root": args.mc_root,
"core_root": args.core_root,
"classifications_source": classifications_path,
"total_files_in_core": total_files,
"files_with_counterpart": files_with_counterpart,
"files_without_counterpart": files_without_counterpart,
"passed": len(summary["PASS"]),
"warned": len(summary["WARN"]),
"failed": len(summary["FAIL"]),
},
"results": results,
"summary": summary,
}
if args.json_output:
json.dump(output, sys.stdout, indent=2)
print()
return 0
# Human-readable output
print()
print(f"Batch Migration Diff — {total_files} core files analyzed")
print(f" With counterpart: {files_with_counterpart}")
print(f" Without counterpart: {files_without_counterpart}")
print()
print(f" PASS: {len(summary['PASS']):>4}")
print(f" WARN: {len(summary['WARN']):>4}")
print(f" FAIL: {len(summary['FAIL']):>4}")
print()
if summary["FAIL"]:
print("FAILED FILES:")
for rel in summary["FAIL"]:
entry = next(r for r in results if r["relative_path"] == rel)
print(f" {rel}")
if entry.get("missing_methods"):
methods = ", ".join(m["name"] for m in entry["missing_methods"])
print(
f" - {len(entry['missing_methods'])} missing methods: {methods}"
)
if entry.get("unmigrated"):
types = ", ".join(u["mc_name"] for u in entry["unmigrated"])
print(f" - {len(entry['unmigrated'])} unmigrated types: {types}")
print()
if summary["WARN"]:
shown = min(5, len(summary["WARN"]))
print(f"WARNED FILES (showing first {shown}):")
for rel in summary["WARN"][:shown]:
entry = next(r for r in results if r["relative_path"] == rel)
parts = []
if entry.get("semantic_body_diffs"):
sims = [
str(round(d["similarity_ratio"] * 100)) + "%"
for d in entry["semantic_body_diffs"]
]
parts.append(
f"{len(entry['semantic_body_diffs'])} body diffs (similarity: {', '.join(sims)})"
)
if entry.get("unmigrated"):
parts.append(f"{len(entry['unmigrated'])} unmigrated types")
print(f" {rel}")
for part in parts:
print(f" ~ {part}")
if len(summary["WARN"]) > shown:
print(f" ... and {len(summary['WARN']) - shown} more")
print()
# Write JSON output file
output_path = project_root / args.output
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2)
print(f"Results written to: {args.output}")
print()
return 0
if __name__ == "__main__":
sys.exit(main())
+385
View File
@@ -0,0 +1,385 @@
"""
generate_pa_manifest.py — Generate PA Interface Manifest + Swap Table
=====================================================================
Generates a pa_batch.py-compatible manifest JSON for creating PA interface
files in platform-api/, plus a swap table for Symbol-Swap.
Usage:
python tools/migration/generate_pa_manifest.py
python tools/migration/generate_pa_manifest.py --category INTERFACE
python tools/migration/generate_pa_manifest.py --category INTERFACE EXISTING
python tools/migration/generate_pa_manifest.py --output tools/migration/_pa_manifest.json
python tools/migration/generate_pa_manifest.py --swap-output tools/migration/_swap_table.json
python tools/migration/generate_pa_manifest.py --json
python tools/migration/generate_pa_manifest.py --dry-run
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime, timezone
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from pa_rules import (
resolve_param_type,
should_exclude_method,
resolve_obfuscated_method,
get_interface_package,
NAMING,
EXISTING_MAPPINGS,
TRANSFORM_PATTERNS,
SERVICE_ROUTING,
)
def auto_detect_project_root():
return SCRIPT_DIR.parent.parent
def _normalize_classifications(data):
"""Normalize both mc_type_mappings.json and mc_type_classifications_original.json formats."""
types = data.get("types") or data.get("mappings") or {}
normalized = {}
for type_name, info in types.items():
if type_name.startswith("_"):
normalized[type_name] = info
continue
entry = dict(info)
if "pa_name" in entry and "pa" not in entry:
entry["pa"] = entry["pa_name"]
if "pa_package" in entry and "package" not in entry:
entry["package"] = entry["pa_package"]
if "service_name" in entry and "service" not in entry:
entry["service"] = entry["service_name"]
normalized[type_name] = entry
return normalized
def _load_classifications_data(project_root, mappings_path=None):
if mappings_path:
p = Path(mappings_path)
if not p.is_absolute():
p = project_root / mappings_path
if p.exists():
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
return _normalize_classifications(data), str(p.name)
for candidate in [
"tools/migration/mc_type_mappings.json",
"tools/migration/mc_type_classifications_original.json",
]:
p = project_root / candidate
if p.exists():
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
return _normalize_classifications(data), candidate
raise FileNotFoundError("No classifications file found")
def _load_methods_export(project_root, methods_path=None):
"""Load all_methods_export.json for usage counts."""
if methods_path:
p = Path(methods_path)
if not p.is_absolute():
p = project_root / methods_path
else:
p = project_root / "tools" / "migration" / "all_methods_export.json"
if not p.exists():
return {}
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
lookup = {}
for t in data.get("types", []):
lookup[t["shortName"]] = t
return lookup
def _infer_return_type(method_name):
"""Infer return type from Java naming conventions."""
if method_name.startswith("get"):
return "Object"
if method_name.startswith("is") or method_name.startswith("has"):
return "boolean"
if (
method_name.startswith("set")
or method_name.startswith("add")
or method_name.startswith("remove")
):
return "void"
return "void"
def _get_usage_count(method_name, methods_export_entry):
"""Look up usage count for a method from the export data."""
if not methods_export_entry:
return 0
for m in methods_export_entry.get("methods", []):
if m["name"] == method_name:
return m["count"]
for m in methods_export_entry.get("static_methods", []):
if m["name"] == method_name:
return m["count"]
return 0
def _generate_interface_content(type_name, info, methods_export_entry):
"""Generate Java interface source code for a PA interface."""
pa_name = info.get("pa", NAMING.get(type_name, f"I{type_name}"))
fqn = info.get("fqn", "")
pkg = info.get("package", get_interface_package(pa_name))
raw_methods = info.get("methods", [])
filtered_methods = []
for m in raw_methods:
method_name = m if isinstance(m, str) else m.get("name", m)
if should_exclude_method(method_name):
continue
resolved_name = resolve_obfuscated_method(method_name)
if resolved_name != method_name and should_exclude_method(resolved_name):
continue
filtered_methods.append(resolved_name)
seen = set()
unique_methods = []
for m in filtered_methods:
if m not in seen:
seen.add(m)
unique_methods.append(m)
lines = []
lines.append(f"package {pkg};")
lines.append("")
lines.append("/**")
lines.append(f" * Platform abstraction for {fqn}.")
lines.append(f" * MC 1.7.10: {type_name}")
lines.append(f" * Generated by generate_pa_manifest.py")
lines.append(" */")
lines.append(f"public interface {pa_name} {{")
for method_name in unique_methods:
usage = _get_usage_count(method_name, methods_export_entry)
ret_type = _infer_return_type(method_name)
usage_note = f" — used {usage} times" if usage > 0 else ""
lines.append(f" /** {method_name}{usage_note} */")
lines.append(f" {ret_type} {method_name}();")
lines.append("}")
lines.append("")
return "\n".join(lines), pa_name, pkg
def _build_swap_entry(type_name, info):
"""Build a swap tuple [old_name, new_name, old_fqn, new_fqn]."""
pa_name = info.get("pa", NAMING.get(type_name, f"I{type_name}"))
fqn = info.get("fqn", "")
pkg = info.get("package", get_interface_package(pa_name))
new_fqn = f"{pkg}.{pa_name}"
return [type_name, pa_name, fqn, new_fqn]
def _build_transform_entries(classifications):
"""Build transform entries from TRANSFORM-category types."""
transforms = []
for type_name, info in classifications.items():
if type_name.startswith("_"):
continue
if info.get("category") != "TRANSFORM":
continue
patterns = info.get("patterns", [])
for pat in patterns:
if len(pat) >= 2:
transforms.append(pat)
for svc_name, routing in SERVICE_ROUTING.items():
if routing.get("service") == "NBTIO":
pass
return transforms
def _format_powershell_swaps(swaps):
"""Format swaps as a PowerShell array string."""
lines = ["$AllSwaps = @("]
for i, swap in enumerate(swaps):
comma = "," if i < len(swaps) - 1 else ""
lines.append(
f" @('{swap[0]}', '{swap[1]}', '{swap[2]}', '{swap[3]}'){comma}"
)
lines.append(")")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Generate PA interface manifest and swap table"
)
parser.add_argument(
"--mappings",
help="Path to mc_type_mappings.json or classifications JSON",
)
parser.add_argument(
"--methods",
default=None,
help="Path to all_methods_export.json",
)
parser.add_argument(
"--category",
nargs="+",
default=["INTERFACE"],
help="Categories to include (default: INTERFACE)",
)
parser.add_argument(
"--output",
default="tools/migration/_pa_manifest.json",
help="Output manifest JSON path (relative to project root)",
)
parser.add_argument(
"--swap-output",
default="tools/migration/_swap_table.json",
help="Output swap table JSON path (relative to project root)",
)
parser.add_argument(
"--json",
dest="json_output",
action="store_true",
help="Print manifest to stdout as JSON",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be generated without writing files",
)
parser.add_argument(
"--project-root",
help="Project root (auto-detected if omitted)",
)
args = parser.parse_args()
project_root = (
Path(args.project_root) if args.project_root else auto_detect_project_root()
)
requested_categories = set(c.upper() for c in args.category)
types, cls_source = _load_classifications_data(project_root, args.mappings)
methods_export = _load_methods_export(project_root, args.methods)
manifest_files = []
all_swaps = []
generated_count = 0
for type_name, info in types.items():
if type_name.startswith("_"):
continue
category = info.get("category", "")
if category not in requested_categories:
continue
pa_name = info.get("pa") or info.get("service")
if not pa_name:
pa_name = NAMING.get(type_name, f"I{type_name}")
if category in ("EXISTING", "INTERFACE"):
swap = _build_swap_entry(type_name, info)
if swap not in all_swaps:
all_swaps.append(swap)
if category == "INTERFACE" and info.get("methods"):
export_entry = methods_export.get(type_name)
content, gen_pa_name, pkg = _generate_interface_content(
type_name, info, export_entry
)
manifest_files.append({"content": content})
generated_count += 1
manifest = {
"root": "platform-api/src/main/java",
"files": manifest_files,
}
all_transforms = _build_transform_entries(types)
swap_table = {
"_meta": {
"generated": datetime.now(timezone.utc).isoformat(),
"source": cls_source,
"total_swaps": len(all_swaps),
"total_transforms": len(all_transforms),
},
"swaps": all_swaps,
"transforms": all_transforms,
"powershell_swaps": _format_powershell_swaps(all_swaps),
}
if args.json_output:
json.dump(manifest, sys.stdout, indent=2)
print()
return 0
if args.dry_run:
print()
print(f"PA Manifest Generator — DRY RUN")
print(f" Source: {cls_source}")
print(f" Categories: {', '.join(sorted(requested_categories))}")
print(f" Interfaces to generate: {generated_count}")
print(f" Swap entries: {len(all_swaps)}")
print(f" Transform entries: {len(all_transforms)}")
print()
if manifest_files:
print("INTERFACES:")
for f in manifest_files:
content = f["content"]
first_line = next(
(
l
for l in content.split("\n")
if l.startswith("public interface")
),
"?",
)
pkg_line = next(
(l for l in content.split("\n") if l.startswith("package ")), "?"
)
print(f" {pkg_line.rstrip(';')} / {first_line.rstrip(' {')}")
print()
if all_swaps:
print("SWAPS:")
for s in all_swaps[:10]:
print(f" {s[0]:30s} -> {s[1]:20s} ({s[2]} -> {s[3]})")
if len(all_swaps) > 10:
print(f" ... and {len(all_swaps) - 10} more")
print()
return 0
manifest_path = project_root / args.output
manifest_path.parent.mkdir(parents=True, exist_ok=True)
with open(manifest_path, "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=2)
swap_path = project_root / args.swap_output
swap_path.parent.mkdir(parents=True, exist_ok=True)
with open(swap_path, "w", encoding="utf-8") as f:
json.dump(swap_table, f, indent=2)
print()
print(f"PA Manifest Generator — {generated_count} interfaces generated")
print(f" Source: {cls_source}")
print(f" Categories: {', '.join(sorted(requested_categories))}")
print(f" Manifest written to: {args.output}")
print(f" Swap table written to: {args.swap_output}")
print(f" Swap entries: {len(all_swaps)}")
print(f" Transform entries: {len(all_transforms)}")
print()
return 0
if __name__ == "__main__":
sys.exit(main())
+416
View File
@@ -0,0 +1,416 @@
"""
generate_wave_script.py — Generate a complete .ps1 migration wave script
========================================================================
Reads mc_type_classifications_original.json (or mc_type_mappings.json) and
generates a ready-to-run PowerShell migration wave script.
Usage:
python tools/migration/generate_wave_script.py
python tools/migration/generate_wave_script.py --sources noppes/npcs/controllers/QuestController.java
python tools/migration/generate_wave_script.py --sources-file tools/migration/_wave_sources.txt
python tools/migration/generate_wave_script.py --allow-paths "noppes/npcs/controllers/,noppes/npcs/quests/"
python tools/migration/generate_wave_script.py --output tools/migration/_wave.ps1
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime, timezone
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from pa_rules import (
NAMING,
EXISTING_MAPPINGS,
get_interface_package,
SERVICE_ROUTING,
)
STANDARD_FORBIDDEN_PREFIXES = [
"net.minecraft.",
"cpw.mods.",
"net.minecraftforge.",
"noppes.npcs.entity.",
"noppes.npcs.client.",
"noppes.npcs.mixin.",
"kamkeel.npcs.network.",
"kamkeel.npcs.entity.",
"noppes.npcs.containers.",
"noppes.npcs.scripted.",
"noppes.npcs.janino.",
"noppes.npcs.wrapper.",
"noppes.npcs.blocks.",
"noppes.npcs.items.",
"noppes.npcs.ai.",
]
DEFAULT_STUBS = [
{
"pkg": "noppes.npcs",
"name": "CustomNpcs",
"type": "class",
"body": ' public static java.io.File Dir = new java.io.File(".");\n public static int DefaultInteractLine = 0;',
},
{
"pkg": "noppes.npcs",
"name": "EventHooks",
"type": "class",
"body": " // STUB - event dispatch stays in mc1710",
},
{
"pkg": "noppes.npcs",
"name": "LogWriter",
"type": "class",
"body": " public static void info(String msg) {}\n public static void warn(String msg) {}\n public static void error(String msg) {}\n public static void error(String msg, Throwable e) {}",
},
{
"pkg": "noppes.npcs.controllers",
"name": "SyncController",
"type": "class",
"body": " // STUB - sync stays in mc1710",
},
]
def auto_detect_project_root():
return SCRIPT_DIR.parent.parent
def _normalize_classifications(data):
types = data.get("types") or data.get("mappings") or {}
normalized = {}
for type_name, info in types.items():
if type_name.startswith("_"):
normalized[type_name] = info
continue
entry = dict(info)
if "pa_name" in entry and "pa" not in entry:
entry["pa"] = entry["pa_name"]
if "pa_package" in entry and "package" not in entry:
entry["package"] = entry["pa_package"]
if "service_name" in entry and "service" not in entry:
entry["service"] = entry["service_name"]
normalized[type_name] = entry
return normalized
def _load_classifications_data(project_root, mappings_path=None):
if mappings_path:
p = Path(mappings_path)
if not p.is_absolute():
p = project_root / mappings_path
if p.exists():
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
return _normalize_classifications(data), str(p.name)
for candidate in [
"tools/migration/mc_type_mappings.json",
"tools/migration/mc_type_classifications_original.json",
]:
p = project_root / candidate
if p.exists():
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
return _normalize_classifications(data), candidate
raise FileNotFoundError("No classifications file found")
def _build_all_swaps(types):
"""Build swap entries from EXISTING + INTERFACE categories."""
swaps = []
seen = set()
for type_name, info in types.items():
if type_name.startswith("_"):
continue
category = info.get("category", "")
if category not in ("EXISTING", "INTERFACE"):
continue
pa_name = info.get("pa")
if not pa_name:
pa_name = NAMING.get(type_name, f"I{type_name}")
fqn = info.get("fqn", "")
pkg = info.get("package", get_interface_package(pa_name))
new_fqn = f"{pkg}.{pa_name}"
key = (type_name, fqn)
if key not in seen:
seen.add(key)
swaps.append((type_name, pa_name, fqn, new_fqn))
return swaps
def _build_all_transforms(types):
"""Build transform entries from TRANSFORM category + SERVICE static-transform patterns."""
transforms = []
for type_name, info in types.items():
if type_name.startswith("_"):
continue
if info.get("category") != "TRANSFORM":
continue
for pat in info.get("patterns", []):
if len(pat) >= 2:
transforms.append((pat[0], pat[1]))
return transforms
def _ps_array_literal(items, indent=" "):
"""Format a list of strings as a PowerShell @() array literal."""
if not items:
return "@()"
lines = ["@("]
for i, item in enumerate(items):
comma = "," if i < len(items) - 1 else ""
lines.append(f"{indent} '{item}'{comma}")
lines.append(f"{indent})")
return "\n".join(lines)
def _ps_swap_array(swaps, indent=" "):
"""Format swap entries as a PowerShell @(@(...), @(...)) array."""
if not swaps:
return "@()"
lines = ["@("]
for i, (old, new, old_fqn, new_fqn) in enumerate(swaps):
comma = "," if i < len(swaps) - 1 else ""
lines.append(
f"{indent} @('{old}', '{new}', '{old_fqn}', '{new_fqn}'){comma}"
)
lines.append(f"{indent})")
return "\n".join(lines)
def _ps_transform_array(transforms, indent=" "):
"""Format transform entries as a PowerShell @(@(...), @(...)) array."""
if not transforms:
return "@()"
lines = ["@("]
for i, (pattern, replacement) in enumerate(transforms):
comma = "," if i < len(transforms) - 1 else ""
lines.append(f"{indent} @('{pattern}', '{replacement}'){comma}")
lines.append(f"{indent})")
return "\n".join(lines)
def _ps_stubs_array(stubs, indent=" "):
"""Format stubs as a PowerShell @(@{...}, @{...}) array."""
if not stubs:
return "@()"
lines = ["@("]
for i, stub in enumerate(stubs):
comma = "," if i < len(stubs) - 1 else ""
body_escaped = stub["body"].replace("'", "''")
lines.append(
f"{indent} @{{ pkg='{stub['pkg']}'; name='{stub['name']}'; type='{stub['type']}';"
)
lines.append(f"{indent} body='{body_escaped}' }}{comma}")
lines.append(f"{indent})")
return "\n".join(lines)
def generate_script(
sources,
allow_paths,
allow_recurse_paths,
mc_root,
swaps,
transforms,
stubs,
forbidden_prefixes,
cls_source,
):
now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
separator = "# " + "=" * 60
sources_arr = _ps_array_literal(sources, " ")
allow_paths_arr = _ps_array_literal(allow_paths, " ") if allow_paths else None
allow_recurse_arr = (
_ps_array_literal(allow_recurse_paths, " ") if allow_recurse_paths else None
)
forbidden_arr = _ps_array_literal(forbidden_prefixes, "")
swaps_arr = _ps_swap_array(swaps, "")
transforms_arr = _ps_transform_array(transforms, "")
stubs_arr = _ps_stubs_array(stubs, "")
lines = []
lines.append(separator)
lines.append(f"# Wave Migration Script")
lines.append(f"# Generated by generate_wave_script.py on {now}")
lines.append(f"# Source: {cls_source}")
lines.append(separator)
lines.append("")
lines.append(". (Join-Path $PSScriptRoot 'Copy-Recursive.ps1')")
lines.append(". (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')")
lines.append(". (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')")
lines.append(". (Join-Path $PSScriptRoot 'Static-Transform.ps1')")
lines.append(". (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')")
lines.append(". (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')")
lines.append(". (Join-Path $PSScriptRoot 'Build-Report.ps1')")
lines.append("")
if sources:
lines.append(separator)
lines.append("# STEP 1: Copy files from mc1710 to core")
lines.append(separator)
copy_cmd = "Copy-Recursive `\n"
copy_cmd += f" -Sources {sources_arr} `\n"
if allow_paths_arr:
copy_cmd += f" -AllowPaths {allow_paths_arr} `\n"
if allow_recurse_arr:
copy_cmd += f" -AllowRecursePaths {allow_recurse_arr} `\n"
copy_cmd += f" -McRoot '{mc_root}'"
lines.append(copy_cmd)
lines.append("")
lines.append(separator)
lines.append("# STEP 2: Nuke MC imports")
lines.append(separator)
lines.append(
f"Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes {forbidden_arr}"
)
lines.append("")
lines.append(separator)
lines.append("# STEP 3: Symbol-Swap — replace MC types with PA abstractions")
lines.append(separator)
lines.append(f"Symbol-Swap -Directory 'core/src/main/java' -Swaps {swaps_arr}")
lines.append("")
if transforms:
lines.append(separator)
lines.append("# STEP 4: Static-Transform — regex rewrites")
lines.append(separator)
lines.append(
f"Static-Transform -Directory 'core/src/main/java' -Transforms {transforms_arr}"
)
lines.append("")
lines.append(separator)
lines.append("# STEP 5: Generate stubs for missing dependencies")
lines.append(separator)
lines.append(f"Generate-Stubs -Stubs {stubs_arr}")
lines.append("")
lines.append(separator)
lines.append("# STEP 6: Deduplicate imports")
lines.append(separator)
lines.append("Dedupe-Imports -Directory 'core/src/main/java'")
lines.append("")
lines.append(separator)
lines.append("# STEP 7: Build and report")
lines.append(separator)
lines.append("Build-Report -GradleTask ':core:compileJava'")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Generate a complete .ps1 migration wave script"
)
parser.add_argument(
"--mappings",
help="Path to mc_type_mappings.json or classifications JSON",
)
parser.add_argument(
"--sources",
nargs="*",
default=[],
help="Java source files to migrate (relative to mc-root)",
)
parser.add_argument(
"--sources-file",
help="Text file with one source path per line",
)
parser.add_argument(
"--allow-paths",
help="Comma-separated AllowPaths prefixes",
)
parser.add_argument(
"--allow-recurse-paths",
help="Comma-separated AllowRecursePaths prefixes",
)
parser.add_argument(
"--mc-root",
default="src/main/java",
help="MC source root (default: src/main/java)",
)
parser.add_argument(
"--output",
default="tools/migration/_wave.ps1",
help="Output .ps1 file path (relative to project root)",
)
parser.add_argument(
"--project-root",
help="Project root (auto-detected if omitted)",
)
args = parser.parse_args()
project_root = (
Path(args.project_root) if args.project_root else auto_detect_project_root()
)
types, cls_source = _load_classifications_data(project_root, args.mappings)
sources = list(args.sources)
if args.sources_file:
sf = Path(args.sources_file)
if not sf.is_absolute():
sf = project_root / args.sources_file
if sf.exists():
with open(sf, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
sources.append(line)
allow_paths = None
if args.allow_paths:
allow_paths = [p.strip() for p in args.allow_paths.split(",") if p.strip()]
allow_recurse_paths = None
if args.allow_recurse_paths:
allow_recurse_paths = [
p.strip() for p in args.allow_recurse_paths.split(",") if p.strip()
]
swaps = _build_all_swaps(types)
transforms = _build_all_transforms(types)
script_content = generate_script(
sources=sources,
allow_paths=allow_paths,
allow_recurse_paths=allow_recurse_paths,
mc_root=args.mc_root,
swaps=swaps,
transforms=transforms,
stubs=DEFAULT_STUBS,
forbidden_prefixes=STANDARD_FORBIDDEN_PREFIXES,
cls_source=cls_source,
)
output_path = project_root / args.output
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
f.write(script_content)
print()
print(f"Wave Script Generator")
print(f" Source: {cls_source}")
print(f" Sources: {len(sources)} files")
print(f" Swaps: {len(swaps)} entries")
print(f" Transforms: {len(transforms)} patterns")
print(f" Stubs: {len(DEFAULT_STUBS)} entries")
print(f" Written to: {args.output}")
print()
return 0
if __name__ == "__main__":
sys.exit(main())
File diff suppressed because it is too large. Load diff