Imported from abomb4/super-cheat (
AGENTS.md). Install upstream withnpx skills add abomb4/super-cheat. Copyright stays with the author.
super-cheat Mod - AGENTS.md
Mod Structure
super-cheat/
├── mod.hjson # Mod metadata (name, version, minGameVersion)
├── content/blocks/*.json # Block hjson definitions (loaded by ContentParser)
├── scripts/
│ ├── main.js # Entry point (required by engine)
│ └── super-cheat/ # Module scripts (require'd by main.js)
│ ├── lib.js # Shared utilities (modName, setBuilding, etc.)
│ └── *.js # Per-block JS logic
├── bundles/
│ ├── bundle.properties # English (default)
│ ├── bundle_zh_CN.properties # Chinese
│ └── bundle_ru.properties # Russian
└── sprites/blocks/ # Block sprites (PNG, named by block internal name)
└── effects/ # Effect-category sprites
Loading Order (CRITICAL)
1. mod.hjson parsed → mod metadata loaded
2. content/blocks/*.json parsed → Block instances created (type field resolves Java class)
3. sprites/ packed into atlas
4. scripts/main.js executed → all require()'d scripts run
5. Content.init() + postInit() called on all content
6. ContentInitEvent fired ← best place to cache computed data
7. ClientLoadEvent fired ← UI is ready
Key implication: Scripts run BEFORE ContentInitEvent. So in scripts you can:
- Create blocks with
extend()or reference hjson-defined blocks - Set
buildTypeto define custom Building behavior - Register event listeners for ContentInitEvent (to access finalized content like ammoTypes)
But you CANNOT access block.ammoTypes etc. at script top-level because content isn't initialized yet.
HJSON/JSON Block Definitions
How ContentParser Loads Blocks
- Files in
content/blocks/(.jsonor.hjson) are parsed byContentParser typefield resolves to a Java class:type: ItemSource→mindustry.world.blocks.sandbox.ItemSource- If
typeis omitted and a block with same name exists, it's an override - Block internal name =
modName + "-" + fileNameWithoutExtension(e.g.invincible-cheat-mod-v8-cheat-item) - Fields are mapped to Java class fields via reflection (snake_case hjson → camelCase Java)
consumes: {}section is parsed separately byreadBlockConsumers()requirements: []makes block free; also setsbuildVisibility = shownif present
Common Block Types for type Field
ItemSource / LiquidSource / PowerSource / HeatSource # Sandbox sources
GenericCrafter / HeatCrafter / AttributeCrafter # Factories
Drill / BeamDrill # Mining
StorageBlock / CoreBlock # Storage
ConsumeGenerator / ThermalGenerator / NuclearReactor # Power
ItemTurret / LiquidTurret / PowerTurret # Turrets
OverdriveProjector / MendProjector / ForceProjector # Projectors
Separator # Processing
When to Use HJSON vs JS-only
- HJSON only: Block uses existing Java class with no custom behavior (e.g. HeatSource, ItemSource)
- HJSON + JS: Block needs custom Building behavior (override updateTile, draw, etc.)
- JS-only (extend): Block class itself needs overrides (e.g. custom drawPlace, load)
JavaScript Scripting
Engine: Rhino (not Node.js)
importPackage()already called for all mindustry packages (seeglobal.js)extend(BaseClass, "name", overrides)→ creates new Block subclassJavaAdapter(JavaClass, overrides, constructorArgs...)→ creates instance with method overridescons(fn),prov(fn),boolf(fn),func(fn)→ create Java functional interface wrappersrequire('super-cheat/module')→ loadsscripts/super-cheat/module.js(Rhino module system)
lib.js Utilities
const lib = require('super-cheat/lib');
lib.modName // "invincible-cheat-mod-v8"
lib.setBuilding(block, creator) // block.buildType = prov(() => creator(block))
lib.setBuildingSimple(block, BuildingClass, overrides)
// block.buildType = prov(() => new JavaAdapter(BuildingClass, overrides, block))
lib.loadRegion(name) // Core.atlas.find(modName + '-' + name)
lib.getMessage(type, key) // Core.bundle.get(type + "." + modName + "." + key)
Two Patterns for Custom Blocks
Pattern A: HJSON defines block, JS adds behavior (most common)
// content/blocks/my-block.json: { type: GenericCrafter, size: 2, ... }
// scripts/super-cheat/my-block.js:
const lib = require('super-cheat/lib');
const block = Vars.content.block("invincible-cheat-mod-v8-my-block");
lib.setBuildingSimple(block, GenericCrafter.GenericCrafterBuild, {
updateTile() { /* custom logic */ },
});
Pattern B: JS creates block with extend() (when block class itself needs overriding)
const blockType = extend(Block, "my-block", {
load() { this.super$load(); /* custom load */ },
drawPlace(x, y, rotation, valid) { /* custom placement preview */ },
});
// Then set building:
lib.setBuilding(blockType, (block) => new JavaAdapter(Building, {
updateTile() { /* custom logic */ },
}, block));
Accessing Block by Name
const block = Vars.content.block("invincible-cheat-mod-v8-my-block");
// Or iterate all blocks:
var iter = Vars.content.blocks().iterator();
while (iter.hasNext()) { var b = iter.next(); /* ... */ }
Bundle Keys Format
block.<modName>-<fileName>.name = Display Name
block.<modName>-<fileName>.description = Description text
unit.<modName>-<fileName>.name = Unit Name
message.<modName>-<key> = Message text
Key Events (from EventType.java)
| Event | When | Use Case |
|---|---|---|
ContentInitEvent |
After all content init+postInit | Cache computed data (best ammo, fuel, etc.) |
ClientLoadEvent |
After client fully loaded | UI setup, one-time initialization |
WorldLoadEvent |
When a map/game loads | Reset per-world state |
ModContentLoadEvent |
After mod hjson parsed, before init | Modify parsed content |
BlockDestroyEvent |
Block destroyed | Cleanup |
TileChangeEvent |
Tile changed | Cache invalidation (use Vars.world.tileChanges counter instead for perf) |
Performance: tileChanges Counter
// Vars.world.tileChanges increments on ANY tile change
// Use as cheap cache invalidation instead of TileChangeEvent
var lastChange = -1;
updateTile() {
if (lastChange != Vars.world.tileChanges) {
lastChange = Vars.world.tileChanges;
// rebuild target list
}
// use cached targets
}
Performance: Timer Pattern
// Run logic every N ticks instead of every tick
var timerIdx = block.timers++; // allocate timer slot
updateTile() {
if (!this.timer.get(timerIdx, 5)) return; // every 5 ticks
// ... logic
}
Arc Library Gotchas (no source in repo — these are hard-won lessons)
Arc is Anuken's private framework (arc.* packages). Source is NOT in this repo. Below are pitfalls discovered through crashes and trial.
Arc Collections (arc.struct.*)
| Type | Key Gotcha |
|---|---|
Seq<T> |
.size is a field not a method. seq.size works, seq.size() does NOT. |
Seq<T> |
Iterate with seq.iterator() + hasNext()/next(), or index loop for(i=0;i<seq.size;i++) seq.get(i). JS for-of does NOT work. |
ObjectMap<K,V> |
.entries() returns ObjectMap$MapIterator — a package-private inner class. Rhino throws IllegalAccessException on any access. NEVER use .entries(). |
ObjectMap<K,V> |
.keys() also returns MapIterator subclass — ALSO CRASHES in Rhino! .keys().toSeq() does NOT work either. |
ObjectMap<K,V> |
Safe iteration: iterate content.items()/liquids() + map.get(key) != null check. Or use map.each(cons2((k,v) => ...)) with lib.cons2(). |
ObjectMap<K,V> |
.size is a field not a method. |
ObjectMap<K,V> |
.get(key) is safe — returns null if not found. .containsKey(key) is safe. |
OrderedMap<K,V> |
Extends ObjectMap, same pitfalls. ammoTypes is OrderedMap. |
ObjectSet<T> |
Iterate with .iterator(), NOT for-of. .size is a field. |
IntSet |
.iterator() returns IntSetIterator (inner class) — same IllegalAccessException as ObjectMap! Use .toSeq() first. |
ObjectFloatMap<K> |
.entries() AND .keys() same problem as ObjectMap. Safe: iterate known keys + .get(key, default). |
Block Public Fields (JS can access directly)
| Field/Method | Type | Notes |
|---|---|---|
block.hasItems |
boolean |
Does block hold items? |
block.hasLiquids |
boolean |
Does block hold liquids? |
block.itemCapacity |
int |
Max total item capacity |
block.liquidCapacity |
float |
Max total liquid capacity |
block.itemFilter[] |
boolean[] |
itemFilter[item.id] = true → block consumes this item |
block.liquidFilter[] |
boolean[] |
liquidFilter[liquid.id] = true → block consumes this liquid |
block.consumesItem(Item) |
boolean |
Method: checks itemFilter[id] |
block.consumesLiquid(Liquid) |
boolean |
Method: checks liquidFilter[id]. Requires Liquid param! No no-arg version! |
block.consumers |
Consume[] |
All consumers (Java array, use .length and [i]) |
block.optionalConsumers |
Consume[] |
Optional/boost consumers |
block.nonOptionalConsumers |
Consume[] |
Required consumers |
block.findConsumer(Boolf<Consume>) |
Consume |
Find first consumer matching predicate. Use boolf(c => ...) |
Java Arrays in Rhino
| Gotcha | Details |
|---|---|
.length not .size |
Java arrays (e.g. Consume[], ItemStack[]) use .length, NOT .size. .size is undefined! |
[i] not .get(i) |
Java arrays use bracket indexing arr[i], NOT .get(i). .get() is undefined! |
| Common arrays | block.nonOptionalConsumers → Consume[], block.optionalConsumers → Consume[], ConsumeItems.items → ItemStack[], ConsumeLiquids.liquids → LiquidStack[] |
Arc Functional Interfaces (arc.func.*)
Rhino cannot auto-convert JS functions to Arc's functional interfaces. You MUST use wrappers:
cons(fn) // → new Cons(){ get: fn }
prov(fn) // → new Prov(){ get: fn }
boolf(fn) // → new Boolf(){ get: fn }
func(fn) // → new Func(){ get: fn }
floatf(fn) // → new Floatf(){ get: fn }
run(fn) // → new Runnable(){ run: fn }
This is already defined in global.js but easy to forget when passing lambdas to Java APIs.
Arc Files (arc.files.Fi)
Fiis NOTjava.io.File. It's Arc's abstraction over filesystem.Fi.readString(),Fi.child("path"),Fi.exists(),Fi.extension()are common methods.Core.filesis the entry point for internal files.
Arc Math (arc.math.*)
Mathfhas static math utilities (lerp, clamp, map, etc.)Geom/Geometryfor geometric operationsTmp.r1/Tmp.v1etc. are reusable temp objects — do NOT store references to them, they get overwritten
Arc Drawing (arc.graphics.g2d.*)
Drawf.dashSquare(color, x, y, size)for range indicatorsDraw.rect(region, x, y, rotation)for block spritesPal.*for game palette colors (e.g.Pal.reactorPurple)Core.atlas.find(name)for sprite regions; returnsCore.atlas.find("error")if not found
Arc Events (arc.Events)
Events.on(EventClass, cons(handler))— handler MUST be wrapped incons()Events.fire(new SomeEvent())to fire events- Events are synchronous; handlers run immediately in fire() call
Common Pitfalls
block.ammoTypesis empty at script load time — must access inContentInitEventextend()block name must match hjson filename — both create the same internal namehandleLiquid(source, liquid, amount)— source must be a Building (usethisfrom updateTile)items.add(item, amount)bypasses capacity — check capacity yourselfJavaAdapteronly overrides methods — useextend()or set fields directly for properties- Sprite filename =
modName + '-' + blockFileName— e.g.invincible-cheat-mod-v8-smart-filler.png block.timers++in script top-level — must happen once, not per building instanceSeqiteration — use.iterator()+.hasNext()/.next(), or index loop; NOT JS for-ofObjectMapiteration —.entries()AND.keys()BOTH CRASH withIllegalAccessException! Safe: iteratecontent.items()/liquids()+map.get(key) != null, ormap.each(lib.cons2((k,v) => ...))instanceofworks —b instanceof ItemTurret.ItemTurretBuildchecks Java typeIntSet.iterator()CRASHES — same inner class issue as ObjectMap. Use.toSeq()first.Tmp.*temp objects —Tmp.r1,Tmp.v1etc. are reused globally. Never store references.- Java arrays —
Consume[],ItemStack[]etc. use.lengthand[i], NOT.size/.get(i). block.coolantisConsumeLiquidBase— not necessarilyConsumeCoolant. Checkinstanceof ConsumeLiquidFilterbefore accessing.filter.BulletType.estimateDPS()does NOT include lightning damage — must addlightning * (lightningDamage < 0 ? damage : lightningDamage) * 0.1manually.BulletType.reloadMultiplier— speeds up reload for specific ammo (e.g. Swarm turret). Factor into DPS asshotsPerSec * reloadMultiplier.- Generator fuel: power output vs total energy —
score = powerProduction * efficiencyMultiplierfor highest power output. Do NOT multiply byitemDuration * itemDurationMultiplier(that gives total energy, not power). ConsumeItemDynamic/ConsumeLiquidsDynamic— items/liquids are dynamic functions of the building state (e.g.UnitFactoryplan). Callcon.items.get(b)/con.liquids.get(b)at runtime, not cached.ConsumePayloadDynamic—con.payloads.get(b)returnsSeq<PayloadStack>. Fill viab.blocks.add(stack.item, stack.amount).block.optionalConsumers— boost/optional consumers (e.g. phase item for OverdriveProjector, coolant for ForceProjector). Must fill these too!ForceProjector.coolantConsumer— separate field fromblock.coolant. Access directly asblock.coolantConsumer.block.explodeOnFull—ConsumeGenerator/GenericCrafterwith this flag +outputLiquid= neoplasm risk. Auto-remove neoplasm liquid.UnitFactoryBuild.getMaximumAccepted(item)— returns per-item capacity (notblock.itemCapacity). Use for correct fill amounts.