diff --git a/.gitignore b/.gitignore index 84b34a12..95e972f1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,7 @@ drool/* # Transpiler output ts-output/ -transpiler/ts-output/ \ No newline at end of file +transpiler/ts-output/ + +# Rust build output +tools/effect-miner/target/ \ No newline at end of file diff --git a/script/DeployEffectsCreate3.s.sol b/script/DeployEffectsCreate3.s.sol new file mode 100644 index 00000000..a94679a0 --- /dev/null +++ b/script/DeployEffectsCreate3.s.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +import {CreateX} from "../src/lib/CreateX.sol"; +import {EffectDeployer} from "../src/lib/EffectDeployer.sol"; +import {EffectBitmap} from "../src/lib/EffectBitmap.sol"; +import {IEngine} from "../src/IEngine.sol"; +import {IEffect} from "../src/effects/IEffect.sol"; + +// Effects +import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; +import {StatBoosts} from "../src/effects/StatBoosts.sol"; +import {Overclock} from "../src/effects/battlefield/Overclock.sol"; +import {BurnStatus} from "../src/effects/status/BurnStatus.sol"; +import {FrostbiteStatus} from "../src/effects/status/FrostbiteStatus.sol"; +import {PanicStatus} from "../src/effects/status/PanicStatus.sol"; +import {SleepStatus} from "../src/effects/status/SleepStatus.sol"; +import {ZapStatus} from "../src/effects/status/ZapStatus.sol"; + +/// @title DeployEffectsCreate3 +/// @notice Deploy Effect contracts via CREATE3 with bitmap-encoded addresses +/// @dev Salts should be pre-mined using the effect-miner CLI tool. +/// Run: effect-miner mine-all --config effects.json --output salts.json +/// +/// Effect bitmaps encode which EffectSteps they run at: +/// - StaminaRegen: 0x042 (RoundEnd, AfterMove) +/// - StatBoosts: 0x008 (OnMonSwitchOut) +/// - Overclock: 0x170 (OnApply, RoundEnd, OnMonSwitchIn, OnRemove) +/// - BurnStatus: 0x1E0 (OnApply, RoundStart, RoundEnd, OnRemove) +/// - FrostbiteStatus: 0x160 (OnApply, RoundEnd, OnRemove) +/// - PanicStatus: 0x1E0 (OnApply, RoundStart, RoundEnd, OnRemove) +/// - SleepStatus: 0x1E0 (OnApply, RoundStart, RoundEnd, OnRemove) +/// - ZapStatus: 0x1E0 (OnApply, RoundStart, RoundEnd, OnRemove) +contract DeployEffectsCreate3 is Script { + /// @notice Canonical CreateX address (same on all EVM chains) + address constant CREATEX_ADDRESS = 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed; + + /// @notice Effect bitmap constants + uint16 constant BITMAP_STAMINA_REGEN = 0x042; // RoundEnd, AfterMove + uint16 constant BITMAP_STAT_BOOSTS = 0x008; // OnMonSwitchOut + uint16 constant BITMAP_OVERCLOCK = 0x170; // OnApply, RoundEnd, OnMonSwitchIn, OnRemove + uint16 constant BITMAP_BURN_STATUS = 0x1E0; // OnApply, RoundStart, RoundEnd, OnRemove + uint16 constant BITMAP_FROSTBITE_STATUS = 0x160; // OnApply, RoundEnd, OnRemove + uint16 constant BITMAP_PANIC_STATUS = 0x1E0; // OnApply, RoundStart, RoundEnd, OnRemove + uint16 constant BITMAP_SLEEP_STATUS = 0x1E0; // OnApply, RoundStart, RoundEnd, OnRemove + uint16 constant BITMAP_ZAP_STATUS = 0x1E0; // OnApply, RoundStart, RoundEnd, OnRemove + + struct EffectSalts { + bytes32 staminaRegen; + bytes32 statBoosts; + bytes32 overclock; + bytes32 burnStatus; + bytes32 frostbiteStatus; + bytes32 panicStatus; + bytes32 sleepStatus; + bytes32 zapStatus; + } + + struct DeployedEffects { + StaminaRegen staminaRegen; + StatBoosts statBoosts; + Overclock overclock; + BurnStatus burnStatus; + FrostbiteStatus frostbiteStatus; + PanicStatus panicStatus; + SleepStatus sleepStatus; + ZapStatus zapStatus; + } + + /// @notice Deploy all core effects via CREATE3 + /// @param engine The engine contract that effects will interact with + /// @param salts Pre-mined salts for each effect (from effect-miner) + /// @return effects Struct containing all deployed effect addresses + function deployEffects(IEngine engine, EffectSalts memory salts) public returns (DeployedEffects memory effects) { + CreateX createX = CreateX(CREATEX_ADDRESS); + + // Deploy StatBoosts first (dependency for other effects) + effects.statBoosts = StatBoosts( + EffectDeployer.deploy( + createX, + salts.statBoosts, + abi.encodePacked(type(StatBoosts).creationCode, abi.encode(engine)), + BITMAP_STAT_BOOSTS + ) + ); + console.log("StatBoosts deployed at:", address(effects.statBoosts)); + + // Deploy StaminaRegen + effects.staminaRegen = StaminaRegen( + EffectDeployer.deploy( + createX, + salts.staminaRegen, + abi.encodePacked(type(StaminaRegen).creationCode, abi.encode(engine)), + BITMAP_STAMINA_REGEN + ) + ); + console.log("StaminaRegen deployed at:", address(effects.staminaRegen)); + + // Deploy Overclock (depends on StatBoosts) + effects.overclock = Overclock( + EffectDeployer.deploy( + createX, + salts.overclock, + abi.encodePacked(type(Overclock).creationCode, abi.encode(engine, effects.statBoosts)), + BITMAP_OVERCLOCK + ) + ); + console.log("Overclock deployed at:", address(effects.overclock)); + + // Deploy status effects + effects.sleepStatus = SleepStatus( + EffectDeployer.deploy( + createX, + salts.sleepStatus, + abi.encodePacked(type(SleepStatus).creationCode, abi.encode(engine)), + BITMAP_SLEEP_STATUS + ) + ); + console.log("SleepStatus deployed at:", address(effects.sleepStatus)); + + effects.panicStatus = PanicStatus( + EffectDeployer.deploy( + createX, + salts.panicStatus, + abi.encodePacked(type(PanicStatus).creationCode, abi.encode(engine)), + BITMAP_PANIC_STATUS + ) + ); + console.log("PanicStatus deployed at:", address(effects.panicStatus)); + + effects.frostbiteStatus = FrostbiteStatus( + EffectDeployer.deploy( + createX, + salts.frostbiteStatus, + abi.encodePacked(type(FrostbiteStatus).creationCode, abi.encode(engine, effects.statBoosts)), + BITMAP_FROSTBITE_STATUS + ) + ); + console.log("FrostbiteStatus deployed at:", address(effects.frostbiteStatus)); + + effects.burnStatus = BurnStatus( + EffectDeployer.deploy( + createX, + salts.burnStatus, + abi.encodePacked(type(BurnStatus).creationCode, abi.encode(engine, effects.statBoosts)), + BITMAP_BURN_STATUS + ) + ); + console.log("BurnStatus deployed at:", address(effects.burnStatus)); + + effects.zapStatus = ZapStatus( + EffectDeployer.deploy( + createX, + salts.zapStatus, + abi.encodePacked(type(ZapStatus).creationCode, abi.encode(engine)), + BITMAP_ZAP_STATUS + ) + ); + console.log("ZapStatus deployed at:", address(effects.zapStatus)); + } + + /// @notice Preview what addresses effects would be deployed to + /// @param salts Pre-mined salts for each effect + function previewAddresses(EffectSalts memory salts) public view { + CreateX createX = CreateX(CREATEX_ADDRESS); + + console.log("Preview of effect addresses:"); + console.log("----------------------------"); + + address addr; + uint16 bitmap; + + addr = EffectDeployer.computeAddress(createX, salts.staminaRegen); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("StaminaRegen:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.statBoosts); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("StatBoosts:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.overclock); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("Overclock:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.burnStatus); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("BurnStatus:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.frostbiteStatus); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("FrostbiteStatus:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.panicStatus); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("PanicStatus:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.sleepStatus); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("SleepStatus:", addr, "bitmap:", bitmap); + + addr = EffectDeployer.computeAddress(createX, salts.zapStatus); + bitmap = EffectBitmap.extractBitmap(addr); + console.log("ZapStatus:", addr, "bitmap:", bitmap); + } + + /// @notice Example run function - replace salts with actual mined values + function run() external { + // IMPORTANT: Replace these with actual mined salts from effect-miner! + // These are placeholder values and will NOT produce correct bitmaps. + EffectSalts memory salts = EffectSalts({ + staminaRegen: bytes32(0), // Mine with: effect-miner mine --name StaminaRegen --bitmap 0x042 + statBoosts: bytes32(0), // Mine with: effect-miner mine --name StatBoosts --bitmap 0x008 + overclock: bytes32(0), // Mine with: effect-miner mine --name Overclock --bitmap 0x170 + burnStatus: bytes32(0), // Mine with: effect-miner mine --name BurnStatus --bitmap 0x1E0 + frostbiteStatus: bytes32(0), // Mine with: effect-miner mine --name FrostbiteStatus --bitmap 0x160 + panicStatus: bytes32(0), // Mine with: effect-miner mine --name PanicStatus --bitmap 0x1E0 + sleepStatus: bytes32(0), // Mine with: effect-miner mine --name SleepStatus --bitmap 0x1E0 + zapStatus: bytes32(0) // Mine with: effect-miner mine --name ZapStatus --bitmap 0x1E0 + }); + + // Preview addresses before deployment + previewAddresses(salts); + + // Uncomment to deploy: + // vm.startBroadcast(); + // IEngine engine = IEngine(address(0)); // Replace with actual engine address + // deployEffects(engine, salts); + // vm.stopBroadcast(); + } +} diff --git a/script/EngineAndPeriphery.s.sol b/script/EngineAndPeriphery.s.sol index b2d83ebe..0dc2b46e 100644 --- a/script/EngineAndPeriphery.s.sol +++ b/script/EngineAndPeriphery.s.sol @@ -32,17 +32,49 @@ import {SleepStatus} from "../src/effects/status/SleepStatus.sol"; import {ZapStatus} from "../src/effects/status/ZapStatus.sol"; import {Overclock} from "../src/effects/battlefield/Overclock.sol"; +// CREATE3 deployment +import {CreateX} from "../src/lib/CreateX.sol"; +import {EffectDeployer} from "../src/lib/EffectDeployer.sol"; +import {EffectBitmap} from "../src/lib/EffectBitmap.sol"; + struct DeployData { string name; address contractAddress; } +/// @notice Pre-mined salts for CREATE3 effect deployment +/// @dev These salts produce addresses with correct EffectStep bitmaps when deployed via CreateX. +/// Generate with: effect-miner mine-all --config effects.json --output salts.json +struct EffectSalts { + bytes32 staminaRegen; // Bitmap 0x042: RoundEnd, AfterMove + bytes32 statBoosts; // Bitmap 0x008: OnMonSwitchOut + bytes32 overclock; // Bitmap 0x170: OnApply, RoundEnd, OnMonSwitchIn, OnRemove + bytes32 burnStatus; // Bitmap 0x1E0: OnApply, RoundStart, RoundEnd, OnRemove + bytes32 frostbiteStatus; // Bitmap 0x160: OnApply, RoundEnd, OnRemove + bytes32 panicStatus; // Bitmap 0x1E0: OnApply, RoundStart, RoundEnd, OnRemove + bytes32 sleepStatus; // Bitmap 0x1E0: OnApply, RoundStart, RoundEnd, OnRemove + bytes32 zapStatus; // Bitmap 0x1E0: OnApply, RoundStart, RoundEnd, OnRemove +} + contract EngineAndPeriphery is Script { uint256 constant NUM_MONS = 4; uint256 constant NUM_MOVES = 4; uint256 constant TIMEOUT_DURATION = 60; - + + /// @notice Canonical CreateX address (same on all EVM chains) + address constant CREATEX_ADDRESS = 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed; + + /// @notice Effect bitmap constants + uint16 constant BITMAP_STAMINA_REGEN = 0x042; + uint16 constant BITMAP_STAT_BOOSTS = 0x008; + uint16 constant BITMAP_OVERCLOCK = 0x170; + uint16 constant BITMAP_BURN_STATUS = 0x1E0; + uint16 constant BITMAP_FROSTBITE_STATUS = 0x160; + uint16 constant BITMAP_PANIC_STATUS = 0x1E0; + uint16 constant BITMAP_SLEEP_STATUS = 0x1E0; + uint16 constant BITMAP_ZAP_STATUS = 0x1E0; + DeployData[] deployedContracts; function run() external returns (DeployData[] memory) { @@ -126,4 +158,111 @@ contract EngineAndPeriphery is Script { ZapStatus zapStatus = new ZapStatus(engine); deployedContracts.push(DeployData({name: "ZAP STATUS", contractAddress: address(zapStatus)})); } + + /// @notice Deploy effects via CREATE3 with bitmap-encoded addresses + /// @dev Uses pre-mined salts to deploy effects at addresses that have the correct + /// EffectStep bitmap encoded in their most significant bits. + /// @param engine The engine contract + /// @param salts Pre-mined salts for each effect (from effect-miner) + /// @return staminaRegen The deployed StaminaRegen effect + function deployGameFundamentalsCreate3(Engine engine, EffectSalts memory salts) + public + returns (StaminaRegen staminaRegen) + { + CreateX createX = CreateX(CREATEX_ADDRESS); + + // Deploy StatBoosts first (dependency for other effects) + StatBoosts statBoosts = StatBoosts( + EffectDeployer.deploy( + createX, + salts.statBoosts, + abi.encodePacked(type(StatBoosts).creationCode, abi.encode(engine)), + BITMAP_STAT_BOOSTS + ) + ); + deployedContracts.push(DeployData({name: "STAT BOOSTS", contractAddress: address(statBoosts)})); + + // Deploy StaminaRegen + staminaRegen = StaminaRegen( + EffectDeployer.deploy( + createX, + salts.staminaRegen, + abi.encodePacked(type(StaminaRegen).creationCode, abi.encode(engine)), + BITMAP_STAMINA_REGEN + ) + ); + deployedContracts.push(DeployData({name: "STAMINA REGEN", contractAddress: address(staminaRegen)})); + + // Deploy Overclock (depends on StatBoosts) + Overclock overclock = Overclock( + EffectDeployer.deploy( + createX, + salts.overclock, + abi.encodePacked(type(Overclock).creationCode, abi.encode(engine, statBoosts)), + BITMAP_OVERCLOCK + ) + ); + deployedContracts.push(DeployData({name: "OVERCLOCK", contractAddress: address(overclock)})); + + // Deploy status effects + SleepStatus sleepStatus = SleepStatus( + EffectDeployer.deploy( + createX, + salts.sleepStatus, + abi.encodePacked(type(SleepStatus).creationCode, abi.encode(engine)), + BITMAP_SLEEP_STATUS + ) + ); + deployedContracts.push(DeployData({name: "SLEEP STATUS", contractAddress: address(sleepStatus)})); + + PanicStatus panicStatus = PanicStatus( + EffectDeployer.deploy( + createX, + salts.panicStatus, + abi.encodePacked(type(PanicStatus).creationCode, abi.encode(engine)), + BITMAP_PANIC_STATUS + ) + ); + deployedContracts.push(DeployData({name: "PANIC STATUS", contractAddress: address(panicStatus)})); + + FrostbiteStatus frostbiteStatus = FrostbiteStatus( + EffectDeployer.deploy( + createX, + salts.frostbiteStatus, + abi.encodePacked(type(FrostbiteStatus).creationCode, abi.encode(engine, statBoosts)), + BITMAP_FROSTBITE_STATUS + ) + ); + deployedContracts.push(DeployData({name: "FROSTBITE STATUS", contractAddress: address(frostbiteStatus)})); + + BurnStatus burnStatus = BurnStatus( + EffectDeployer.deploy( + createX, + salts.burnStatus, + abi.encodePacked(type(BurnStatus).creationCode, abi.encode(engine, statBoosts)), + BITMAP_BURN_STATUS + ) + ); + deployedContracts.push(DeployData({name: "BURN STATUS", contractAddress: address(burnStatus)})); + + ZapStatus zapStatus = ZapStatus( + EffectDeployer.deploy( + createX, + salts.zapStatus, + abi.encodePacked(type(ZapStatus).creationCode, abi.encode(engine)), + BITMAP_ZAP_STATUS + ) + ); + deployedContracts.push(DeployData({name: "ZAP STATUS", contractAddress: address(zapStatus)})); + + // Create ruleset with staminaRegen + IEffect[] memory effects = new IEffect[](1); + effects[0] = staminaRegen; + DefaultRuleset ruleset = new DefaultRuleset(engine, effects); + deployedContracts.push(DeployData({name: "DEFAULT RULESET", contractAddress: address(ruleset)})); + + DefaultValidator validator = + new DefaultValidator(engine, DefaultValidator.Args({MONS_PER_TEAM: NUM_MONS, MOVES_PER_MON: NUM_MOVES, TIMEOUT_DURATION: TIMEOUT_DURATION})); + deployedContracts.push(DeployData({name: "DEFAULT VALIDATOR", contractAddress: address(validator)})); + } } diff --git a/snapshots/EngineGasTest.json b/snapshots/EngineGasTest.json index 974b5dd8..0d0e87aa 100644 --- a/snapshots/EngineGasTest.json +++ b/snapshots/EngineGasTest.json @@ -1,17 +1,17 @@ { - "B1_Execute": "973562", - "B1_Setup": "817602", - "B2_Execute": "753779", - "B2_Setup": "279034", - "Battle1_Execute": "494044", + "B1_Execute": "944409", + "B1_Setup": "817597", + "B2_Execute": "724215", + "B2_Setup": "279223", + "Battle1_Execute": "493976", "Battle1_Setup": "794019", - "Battle2_Execute": "408734", - "Battle2_Setup": "235683", - "FirstBattle": "3493668", - "Intermediary stuff": "47036", - "SecondBattle": "3586835", - "Setup 1": "1673954", - "Setup 2": "296769", - "Setup 3": "339643", - "ThirdBattle": "2904295" + "Battle2_Execute": "408666", + "Battle2_Setup": "235686", + "FirstBattle": "3254509", + "Intermediary stuff": "46795", + "SecondBattle": "3353697", + "Setup 1": "1673951", + "Setup 2": "296760", + "Setup 3": "339575", + "ThirdBattle": "2665291" } \ No newline at end of file diff --git a/src/Engine.sol b/src/Engine.sol index e0993c5f..24852dff 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -10,6 +10,7 @@ import "./moves/IMoveSet.sol"; import {IEngine} from "./IEngine.sol"; import {MappingAllocator} from "./lib/MappingAllocator.sol"; import {IMatchmaker} from "./matchmaker/IMatchmaker.sol"; +import {EffectBitmap} from "./lib/EffectBitmap.sol"; contract Engine is IEngine, MappingAllocator { @@ -622,9 +623,18 @@ contract Engine is IEngine, MappingAllocator { ); // Check if we have to run an onApply state update - if (effect.shouldRunAtStep(EffectStep.OnApply)) { + if (EffectBitmap.shouldRunAtStep(address(effect), EffectStep.OnApply)) { + // Create context for onApply + BattleData storage battle = battleData[battleKey]; + EffectContext memory ctx = EffectContext({ + battleKey: battleKey, + p0ActiveMonIndex: uint8(battle.activeMonIndex & 0xFF), + p1ActiveMonIndex: uint8(battle.activeMonIndex >> 8), + playerSwitchForTurnFlag: battle.playerSwitchForTurnFlag, + turnId: battle.turnId + }); // If so, we run the effect first, and get updated extraData if necessary - (extraDataToUse, removeAfterRun) = effect.onApply(tempRNG, extraData, targetIndex, monIndex); + (extraDataToUse, removeAfterRun) = effect.onApply(ctx, tempRNG, extraData, targetIndex, monIndex); } if (!removeAfterRun) { // Add to the appropriate effects mapping based on targetIndex @@ -720,7 +730,7 @@ contract Engine is IEngine, MappingAllocator { return; } - if (effect.shouldRunAtStep(EffectStep.OnRemove)) { + if (EffectBitmap.shouldRunAtStep(address(effect), EffectStep.OnRemove)) { effect.onRemove(data, 2, monIndex); } @@ -748,7 +758,7 @@ contract Engine is IEngine, MappingAllocator { return; } - if (effect.shouldRunAtStep(EffectStep.OnRemove)) { + if (EffectBitmap.shouldRunAtStep(address(effect), EffectStep.OnRemove)) { effect.onRemove(data, targetIndex, monIndex); } @@ -1109,7 +1119,7 @@ contract Engine is IEngine, MappingAllocator { // Skip tombstoned effects if (address(eff.effect) != TOMBSTONE_ADDRESS) { _runSingleEffect( - config, rng, effectIndex, playerIndex, monIndex, round, extraEffectsData, + config, battle, rng, effectIndex, playerIndex, monIndex, round, extraEffectsData, eff.effect, eff.data, uint96(slotIndex) ); } @@ -1120,6 +1130,7 @@ contract Engine is IEngine, MappingAllocator { function _runSingleEffect( BattleConfig storage config, + BattleData storage battle, uint256 rng, uint256 effectIndex, uint256 playerIndex, @@ -1130,7 +1141,7 @@ contract Engine is IEngine, MappingAllocator { bytes32 data, uint96 slotIndex ) private { - if (!effect.shouldRunAtStep(round)) { + if (!EffectBitmap.shouldRunAtStep(address(effect), round)) { return; } @@ -1141,9 +1152,18 @@ contract Engine is IEngine, MappingAllocator { battleKeyForWrite, effectIndex, monIndex, address(effect), data, _getUpstreamCallerAndResetValue(), currentStep ); + // Create effect context to pass to the hook (avoids external callbacks) + EffectContext memory ctx = EffectContext({ + battleKey: battleKeyForWrite, + p0ActiveMonIndex: uint8(battle.activeMonIndex & 0xFF), + p1ActiveMonIndex: uint8(battle.activeMonIndex >> 8), + playerSwitchForTurnFlag: battle.playerSwitchForTurnFlag, + turnId: battle.turnId + }); + // Run the effect and get result (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( - effect, rng, data, playerIndex, monIndex, round, extraEffectsData + ctx, effect, rng, data, playerIndex, monIndex, round, extraEffectsData ); // If we need to remove or update the effect @@ -1153,6 +1173,7 @@ contract Engine is IEngine, MappingAllocator { } function _executeEffectHook( + EffectContext memory ctx, IEffect effect, uint256 rng, bytes32 data, @@ -1162,21 +1183,21 @@ contract Engine is IEngine, MappingAllocator { bytes memory extraEffectsData ) private returns (bytes32 updatedExtraData, bool removeAfterRun) { if (round == EffectStep.RoundStart) { - return effect.onRoundStart(rng, data, playerIndex, monIndex); + return effect.onRoundStart(ctx, rng, data, playerIndex, monIndex); } else if (round == EffectStep.RoundEnd) { - return effect.onRoundEnd(rng, data, playerIndex, monIndex); + return effect.onRoundEnd(ctx, rng, data, playerIndex, monIndex); } else if (round == EffectStep.OnMonSwitchIn) { - return effect.onMonSwitchIn(rng, data, playerIndex, monIndex); + return effect.onMonSwitchIn(ctx, rng, data, playerIndex, monIndex); } else if (round == EffectStep.OnMonSwitchOut) { - return effect.onMonSwitchOut(rng, data, playerIndex, monIndex); + return effect.onMonSwitchOut(ctx, rng, data, playerIndex, monIndex); } else if (round == EffectStep.AfterDamage) { - return effect.onAfterDamage(rng, data, playerIndex, monIndex, abi.decode(extraEffectsData, (int32))); + return effect.onAfterDamage(ctx, rng, data, playerIndex, monIndex, abi.decode(extraEffectsData, (int32))); } else if (round == EffectStep.AfterMove) { - return effect.onAfterMove(rng, data, playerIndex, monIndex); + return effect.onAfterMove(ctx, rng, data, playerIndex, monIndex); } else if (round == EffectStep.OnUpdateMonState) { (uint256 statePlayerIndex, uint256 stateMonIndex, MonStateIndexName stateVarIndex, int32 valueToAdd) = abi.decode(extraEffectsData, (uint256, uint256, MonStateIndexName, int32)); - return effect.onUpdateMonState(rng, data, statePlayerIndex, stateMonIndex, stateVarIndex, valueToAdd); + return effect.onUpdateMonState(ctx, rng, data, statePlayerIndex, stateMonIndex, stateVarIndex, valueToAdd); } } diff --git a/src/Structs.sol b/src/Structs.sol index 95c83389..f27a2235 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -210,6 +210,15 @@ struct CommitContext { address validator; } +// Context passed to effect hooks to avoid external callbacks to Engine +struct EffectContext { + bytes32 battleKey; + uint8 p0ActiveMonIndex; + uint8 p1ActiveMonIndex; + uint8 playerSwitchForTurnFlag; + uint64 turnId; +} + // Batch context for damage calculation to reduce external calls (7 -> 1) struct DamageCalcContext { uint8 attackerMonIndex; diff --git a/src/effects/BasicEffect.sol b/src/effects/BasicEffect.sol index f9f49531..86657005 100644 --- a/src/effects/BasicEffect.sol +++ b/src/effects/BasicEffect.sol @@ -18,7 +18,7 @@ abstract contract BasicEffect is IEffect { } // Lifecycle hooks during normal battle flow - function onRoundStart(uint256, bytes32 extraData, uint256, uint256) + function onRoundStart(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -26,7 +26,7 @@ abstract contract BasicEffect is IEffect { return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -35,7 +35,7 @@ abstract contract BasicEffect is IEffect { } // NOTE: ONLY RUN ON GLOBAL EFFECTS (mons have their Ability as their own hook to apply an effect on switch in) - function onMonSwitchIn(uint256, bytes32 extraData, uint256, uint256) + function onMonSwitchIn(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -44,7 +44,7 @@ abstract contract BasicEffect is IEffect { } // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onMonSwitchOut(uint256, bytes32 extraData, uint256, uint256) + function onMonSwitchOut(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -53,7 +53,7 @@ abstract contract BasicEffect is IEffect { } // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onAfterDamage(uint256, bytes32 extraData, uint256, uint256, int32) + function onAfterDamage(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256, int32) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -61,7 +61,7 @@ abstract contract BasicEffect is IEffect { return (extraData, false); } - function onAfterMove(uint256, bytes32 extraData, uint256, uint256) + function onAfterMove(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -71,7 +71,7 @@ abstract contract BasicEffect is IEffect { // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) // WARNING: Avoid chaining this effect to prevent recursive calls - function onUpdateMonState(uint256, bytes32 extraData, uint256, uint256, MonStateIndexName, int32) + function onUpdateMonState(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256, MonStateIndexName, int32) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -80,7 +80,7 @@ abstract contract BasicEffect is IEffect { } // Lifecycle hooks when being applied or removed - function onApply(uint256, bytes32, uint256, uint256) + function onApply(EffectContext calldata, uint256, bytes32, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index 5a7ce8be..4167775a 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -14,27 +14,28 @@ interface IEffect { function shouldApply(bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bool); // Lifecycle hooks during normal battle flow - function onRoundStart(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + // Note: EffectContext contains battleKey, active mon indices, playerSwitchForTurnFlag, and turnId + function onRoundStart(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onRoundEnd(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onMonSwitchIn(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onMonSwitchOut(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bytes32 updatedExtraData, bool removeAfterRun); // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onAfterDamage(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32 damage) + function onAfterDamage(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32 damage) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onAfterMove(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onAfterMove(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bytes32 updatedExtraData, bool removeAfterRun); @@ -42,6 +43,7 @@ interface IEffect { // WARNING: Avoid chaining this effect to prevent recursive calls // (e.g., an effect that mutates state triggering another effect that mutates state) function onUpdateMonState( + EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 playerIndex, @@ -51,7 +53,7 @@ interface IEffect { ) external returns (bytes32 updatedExtraData, bool removeAfterRun); // Lifecycle hooks when being applied or removed - function onApply(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bytes32 updatedExtraData, bool removeAfterRun); function onRemove(bytes32 extraData, uint256 targetIndex, uint256 monIndex) external; diff --git a/src/effects/StaminaRegen.sol b/src/effects/StaminaRegen.sol index 42d583e3..edcacee3 100644 --- a/src/effects/StaminaRegen.sol +++ b/src/effects/StaminaRegen.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {NO_OP_MOVE_INDEX, MOVE_INDEX_MASK} from "../Constants.sol"; import "../Enums.sol"; -import {MoveDecision} from "../Structs.sol"; +import {MoveDecision, EffectContext} from "../Structs.sol"; import {IEngine} from "../IEngine.sol"; import {BasicEffect} from "./BasicEffect.sol"; @@ -25,40 +25,36 @@ contract StaminaRegen is BasicEffect { } // No overhealing stamina - function _regenStamina(uint256 playerIndex, uint256 monIndex) internal { + function _regenStamina(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) internal { int256 currentActiveMonStaminaDelta = - ENGINE.getMonStateForBattle(ENGINE.battleKeyForWrite(), playerIndex, monIndex, MonStateIndexName.Stamina); + ENGINE.getMonStateForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Stamina); if (currentActiveMonStaminaDelta < 0) { ENGINE.updateMonState(playerIndex, monIndex, MonStateIndexName.Stamina, 1); } } // Regen stamina on round end for both active mons - function onRoundEnd(uint256, bytes32, uint256, uint256) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); - uint256 playerSwitchForTurnFlag = ENGINE.getPlayerSwitchForTurnFlagForBattleState(battleKey); - uint256[] memory activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey); + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32, uint256, uint256) external override returns (bytes32, bool) { + // Use context directly instead of external calls // Update stamina for both active mons only if it's a 2 player turn - if (playerSwitchForTurnFlag == 2) { - for (uint256 playerIndex; playerIndex < 2; ++playerIndex) { - _regenStamina(playerIndex, activeMonIndex[playerIndex]); - } + if (ctx.playerSwitchForTurnFlag == 2) { + _regenStamina(ctx.battleKey, 0, ctx.p0ActiveMonIndex); + _regenStamina(ctx.battleKey, 1, ctx.p1ActiveMonIndex); } return (bytes32(0), false); } // Regen stamina if the mon did a No Op (i.e. resting) - function onAfterMove(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onAfterMove(EffectContext calldata ctx, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); - MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); + MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(ctx.battleKey, targetIndex); // Unpack the move index from packedMoveIndex uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; if (moveIndex == NO_OP_MOVE_INDEX) { - _regenStamina(targetIndex, monIndex); + _regenStamina(ctx.battleKey, targetIndex, monIndex); } return (bytes32(0), false); } diff --git a/src/effects/StatBoosts.sol b/src/effects/StatBoosts.sol index e7cf3448..b26ebefd 100644 --- a/src/effects/StatBoosts.sol +++ b/src/effects/StatBoosts.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import {EffectStep, MonStateIndexName, StatBoostFlag, StatBoostType} from "../Enums.sol"; -import {EffectInstance, MonStats, StatBoostToApply} from "../Structs.sol"; +import {EffectContext, EffectInstance, MonStats, StatBoostToApply} from "../Structs.sol"; import {IEngine} from "../IEngine.sol"; import {BasicEffect} from "./BasicEffect.sol"; @@ -45,7 +45,7 @@ contract StatBoosts is BasicEffect { } // Removes all temporary boosts on mon switch out - function onMonSwitchOut(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(EffectContext calldata, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) diff --git a/src/effects/battlefield/Overclock.sol b/src/effects/battlefield/Overclock.sol index 5618a6a9..c578ee95 100644 --- a/src/effects/battlefield/Overclock.sol +++ b/src/effects/battlefield/Overclock.sol @@ -79,7 +79,7 @@ contract Overclock is BasicEffect { STAT_BOOST.removeStatBoosts(playerIndex, monIndex, StatBoostFlag.Temp); } - function onApply(uint256, bytes32 extraData, uint256, uint256) + function onApply(EffectContext calldata ctx, uint256, bytes32 extraData, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -90,19 +90,19 @@ contract Overclock is BasicEffect { setDuration(DEFAULT_DURATION, playerIndex); // Apply stat change to the team of the player who summoned Overclock - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[playerIndex]; + uint256 activeMonIndex = playerIndex == 0 ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; _applyStatChange(playerIndex, activeMonIndex); return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { uint256 playerIndex = uint256(extraData); - uint256 duration = getDuration(ENGINE.battleKeyForWrite(), playerIndex); + uint256 duration = getDuration(ctx.battleKey, playerIndex); if (duration == 1) { return (extraData, true); } else { @@ -111,7 +111,7 @@ contract Overclock is BasicEffect { } } - function onMonSwitchIn(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(EffectContext calldata, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index 98c681d0..08c3506d 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import "../../Enums.sol"; -import {StatBoostToApply, EffectInstance} from "../../Structs.sol"; +import {StatBoostToApply, EffectInstance, EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {StatBoosts} from "../StatBoosts.sol"; @@ -53,21 +53,20 @@ contract BurnStatus is StatusEffect { return keccak256(abi.encode(targetIndex, monIndex, name())); } - function onApply(uint256 rng, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata ctx, uint256 rng, bytes32, uint256 targetIndex, uint256 monIndex) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); bool hasBurnAlready; { bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - uint192 monStatusFlag = ENGINE.getGlobalKV(battleKey, keyForMon); + uint192 monStatusFlag = ENGINE.getGlobalKV(ctx.battleKey, keyForMon); hasBurnAlready = monStatusFlag == uint192(uint160(address(this))); } // Set burn flag - super.onApply(rng, bytes32(0), targetIndex, monIndex); + super.onApply(ctx, rng, bytes32(0), targetIndex, monIndex); // Set stat debuff or increase burn degree if (!hasBurnAlready) { @@ -80,7 +79,7 @@ contract BurnStatus is StatusEffect { }); STAT_BOOSTS.addStatBoosts(targetIndex, monIndex, statBoosts, StatBoostFlag.Perm); } else { - (EffectInstance[] memory effects, uint256[] memory indices) = ENGINE.getEffects(battleKey, targetIndex, monIndex); + (EffectInstance[] memory effects, uint256[] memory indices) = ENGINE.getEffects(ctx.battleKey, targetIndex, monIndex); uint256 indexOfBurnEffect; uint256 burnDegree; bytes32 newExtraData; @@ -112,7 +111,7 @@ contract BurnStatus is StatusEffect { } // Deal damage over time - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) @@ -126,7 +125,7 @@ contract BurnStatus is StatusEffect { damageDenom = DEG3_DAMAGE_DENOM; } int32 damage = - int32(ENGINE.getMonValueForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp)) + int32(ENGINE.getMonValueForBattle(ctx.battleKey, targetIndex, monIndex, MonStateIndexName.Hp)) / damageDenom; ENGINE.dealDamage(targetIndex, monIndex, damage); return (extraData, false); diff --git a/src/effects/status/FrostbiteStatus.sol b/src/effects/status/FrostbiteStatus.sol index c304c8f1..e30382cf 100644 --- a/src/effects/status/FrostbiteStatus.sol +++ b/src/effects/status/FrostbiteStatus.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import "../../Enums.sol"; -import {StatBoostToApply} from "../../Structs.sol"; +import {StatBoostToApply, EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {StatBoosts} from "../StatBoosts.sol"; @@ -27,13 +27,13 @@ contract FrostbiteStatus is StatusEffect { return (r == EffectStep.OnApply || r == EffectStep.RoundEnd || r == EffectStep.OnRemove); } - function onApply(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, extraData, targetIndex, monIndex); + super.onApply(ctx, rng, extraData, targetIndex, monIndex); // Reduce special attack by half StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); @@ -55,14 +55,14 @@ contract FrostbiteStatus is StatusEffect { STAT_BOOST.removeStatBoosts(targetIndex, monIndex, StatBoostFlag.Perm); } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) public override returns (bytes32, bool) { // Calculate damage to deal uint32 maxHealth = - ENGINE.getMonValueForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp); + ENGINE.getMonValueForBattle(ctx.battleKey, targetIndex, monIndex, MonStateIndexName.Hp); int32 damage = int32(maxHealth) / DAMAGE_DENOM; ENGINE.dealDamage(targetIndex, monIndex, damage); diff --git a/src/effects/status/PanicStatus.sol b/src/effects/status/PanicStatus.sol index 8d349d84..b3ea575d 100644 --- a/src/effects/status/PanicStatus.sol +++ b/src/effects/status/PanicStatus.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {EffectStep, MonStateIndexName} from "../../Enums.sol"; +import {EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {StatusEffect} from "./StatusEffect.sol"; @@ -23,7 +24,7 @@ contract PanicStatus is StatusEffect { } // At the start of the turn, check to see if we should apply stamina debuff or end early - function onRoundStart(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundStart(EffectContext calldata, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external pure override @@ -39,28 +40,26 @@ contract PanicStatus is StatusEffect { } // On apply, checks to apply the flag, and then sets the extraData to be the duration - function onApply(uint256 rng, bytes32 data, uint256 monIndex, uint256 playerIndex) + function onApply(EffectContext calldata ctx, uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, data, monIndex, playerIndex); + super.onApply(ctx, rng, data, targetIndex, monIndex); return (bytes32(DURATION), false); } // Apply effect on end of turn, and then check how many turns are left - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); - // Get current stamina delta of the target mon - int32 staminaDelta = ENGINE.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); + int32 staminaDelta = ENGINE.getMonStateForBattle(ctx.battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); // If the stamina is less than the max stamina, then reduce stamina by 1 (as long as it's not already 0) - uint32 maxStamina = ENGINE.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); + uint32 maxStamina = ENGINE.getMonValueForBattle(ctx.battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); if (staminaDelta + int32(maxStamina) > 0) { ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Stamina, -1); } diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index 2fe4cdaf..8fb87341 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, MOVE_INDEX_MASK} from "../../Constants.sol"; import {EffectStep} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {MoveDecision} from "../../Structs.sol"; +import {MoveDecision, EffectContext} from "../../Structs.sol"; import {StatusEffect} from "./StatusEffect.sol"; @@ -35,8 +35,7 @@ contract SleepStatus is StatusEffect { return (shouldApplyStatusInGeneral && playerHasZeroSleepers); } - function _applySleep(uint256 targetIndex, uint256) internal { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function _applySleep(bytes32 battleKey, uint256 targetIndex, uint256) internal { // Get exiting move index (unpack from packedMoveIndex) MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; @@ -46,35 +45,34 @@ contract SleepStatus is StatusEffect { } // At the start of the turn, check to see if we should apply sleep or end early - function onRoundStart(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundStart(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) { bool wakeEarly = rng % 3 == 0; if (!wakeEarly) { - _applySleep(targetIndex, monIndex); + _applySleep(ctx.battleKey, targetIndex, monIndex); } return (extraData, wakeEarly); } // On apply, checks to apply the sleep flag, and then sets the extraData to be the duration - function onApply(uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata ctx, uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, data, targetIndex, monIndex); + super.onApply(ctx, rng, data, targetIndex, monIndex); // Check if opponent has yet to move and if so, also affect their move for this round - bytes32 battleKey = ENGINE.battleKeyForWrite(); - uint256 priorityPlayerIndex = ENGINE.computePriorityPlayerIndex(battleKey, rng); + uint256 priorityPlayerIndex = ENGINE.computePriorityPlayerIndex(ctx.battleKey, rng); if (targetIndex != priorityPlayerIndex) { - _applySleep(targetIndex, monIndex); + _applySleep(ctx.battleKey, targetIndex, monIndex); } return (bytes32(DURATION), false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external pure override diff --git a/src/effects/status/StatusEffect.sol b/src/effects/status/StatusEffect.sol index 97866f7b..491c0078 100644 --- a/src/effects/status/StatusEffect.sol +++ b/src/effects/status/StatusEffect.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; +import {EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../BasicEffect.sol"; import {StatusEffectLib} from "./StatusEffectLib.sol"; @@ -29,16 +30,15 @@ abstract contract StatusEffect is BasicEffect { } } - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata ctx, uint256, bytes32, uint256 targetIndex, uint256 monIndex) public virtual override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - uint192 monValue = ENGINE.getGlobalKV(battleKey, keyForMon); + uint192 monValue = ENGINE.getGlobalKV(ctx.battleKey, keyForMon); if (monValue == 0) { // Set the global status flag to be the address of the status ENGINE.setGlobalKV(keyForMon, uint192(uint160(address(this)))); diff --git a/src/effects/status/ZapStatus.sol b/src/effects/status/ZapStatus.sol index 6b29e510..0399ec95 100644 --- a/src/effects/status/ZapStatus.sol +++ b/src/effects/status/ZapStatus.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; -import {MoveDecision} from "../../Structs.sol"; +import {MoveDecision, EffectContext} from "../../Structs.sol"; import {NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, MOVE_INDEX_MASK } from "../../Constants.sol"; import {EffectStep, MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; @@ -23,16 +23,15 @@ contract ZapStatus is StatusEffect { || r == EffectStep.OnRemove); } - function onApply(uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata ctx, uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, data, targetIndex, monIndex); + super.onApply(ctx, rng, data, targetIndex, monIndex); - // Get the battle key and compute priority player index - bytes32 battleKey = ENGINE.battleKeyForWrite(); - uint256 priorityPlayerIndex = ENGINE.computePriorityPlayerIndex(battleKey, rng); + // Compute priority player index + uint256 priorityPlayerIndex = ENGINE.computePriorityPlayerIndex(ctx.battleKey, rng); uint8 state; @@ -48,14 +47,13 @@ contract ZapStatus is StatusEffect { return (bytes32(uint256(state)), false); } - function onRoundStart(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onRoundStart(EffectContext calldata ctx, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { // If we're at RoundStart and effect is still present, always set skip flag and mark as skipped, unless the selected move is a switch move - bytes32 battleKey = ENGINE.battleKeyForWrite(); - MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); + MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(ctx.battleKey, targetIndex); uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; if (moveIndex == SWITCH_MOVE_INDEX) { return (bytes32(uint256(0)), false); @@ -68,7 +66,7 @@ contract ZapStatus is StatusEffect { super.onRemove(data, targetIndex, monIndex); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) public pure override diff --git a/src/lib/EffectBitmap.sol b/src/lib/EffectBitmap.sol new file mode 100644 index 00000000..44d0af2f --- /dev/null +++ b/src/lib/EffectBitmap.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {EffectStep} from "../Enums.sol"; + +/// @title EffectBitmap +/// @notice Library for checking effect step eligibility based on address bitmaps +/// @dev The most significant N bits of an effect's address encode which EffectSteps it runs at, +/// where N is the number of steps defined in the EffectStep enum. +/// This allows gas-efficient checks without external calls to shouldRunAtStep(). +/// +/// Bitmap encoding (from address MSB): +/// Bit (NUM_EFFECT_STEPS-1) → First EffectStep (step 0) +/// Bit (NUM_EFFECT_STEPS-2) → Second EffectStep (step 1) +/// ... +/// Bit 0 → Last EffectStep (step NUM_EFFECT_STEPS-1) +/// +/// The bitmap is stored in the most significant bits of the address. For example, +/// if NUM_EFFECT_STEPS=9 and an effect runs at steps 2 and 7, its bitmap would be +/// 0b001000010, and its address would start with 0x21... +/// +/// When adding new EffectSteps to the enum, update NUM_EFFECT_STEPS and re-mine +/// addresses for all effects to include the new step bits. +library EffectBitmap { + /// @notice Number of effect steps in the EffectStep enum + /// @dev Update this constant when adding new steps to the EffectStep enum + uint256 internal constant NUM_EFFECT_STEPS = 9; + + /// @notice Number of bits to shift right to extract the bitmap from an address + /// @dev Address is 160 bits, we want the top NUM_EFFECT_STEPS bits + uint256 internal constant BITMAP_SHIFT = 160 - NUM_EFFECT_STEPS; + + /// @notice Check if an effect should run at a given step based on its address bitmap + /// @param effect The effect contract address (with bitmap encoded in MSB) + /// @param step The EffectStep to check + /// @return True if the effect should run at this step + function shouldRunAtStep(address effect, EffectStep step) internal pure returns (bool) { + // Extract the top NUM_EFFECT_STEPS bits of the address + uint256 bitmap = uint160(effect) >> BITMAP_SHIFT; + + // Check if the bit corresponding to this step is set + // EffectStep enum value N maps to bit (NUM_EFFECT_STEPS - 1 - N) + // So step 0 maps to the highest bit, step (NUM_EFFECT_STEPS-1) maps to bit 0 + uint256 stepBit = 1 << (NUM_EFFECT_STEPS - 1 - uint8(step)); + + return (bitmap & stepBit) != 0; + } + + /// @notice Extract the full bitmap from an effect address + /// @param effect The effect contract address + /// @return The bitmap value (NUM_EFFECT_STEPS bits) + function extractBitmap(address effect) internal pure returns (uint16) { + return uint16(uint160(effect) >> BITMAP_SHIFT); + } + + /// @notice Validate that an effect's address bitmap matches expected value + /// @param effect The effect contract address + /// @param expectedBitmap The expected bitmap value + /// @return True if the bitmap matches + function validateBitmap(address effect, uint16 expectedBitmap) internal pure returns (bool) { + return extractBitmap(effect) == expectedBitmap; + } +} diff --git a/src/lib/EffectDeployer.sol b/src/lib/EffectDeployer.sol new file mode 100644 index 00000000..7fbfbcce --- /dev/null +++ b/src/lib/EffectDeployer.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {CreateX} from "./CreateX.sol"; +import {EffectBitmap} from "./EffectBitmap.sol"; +import {EffectStep} from "../Enums.sol"; + +/// @title EffectDeployer +/// @notice Helper library for deploying Effect contracts via CREATE3 with bitmap-encoded addresses +/// @dev Uses CreateX to deploy effects at addresses where the MSB encodes which EffectSteps they run at. +/// Salts must be pre-mined using the effect-miner CLI tool. +library EffectDeployer { + /// @notice Error thrown when deployed address doesn't match expected bitmap + error BitmapMismatch(address deployed, uint16 expectedBitmap, uint16 actualBitmap); + + /// @notice Deploy an effect contract via CREATE3 and verify its address bitmap + /// @param createX The CreateX factory contract + /// @param salt The pre-mined salt that produces an address with the correct bitmap + /// @param initCode The contract creation bytecode (including constructor args) + /// @param expectedBitmap The expected bitmap value for verification + /// @return deployed The deployed contract address + function deploy( + CreateX createX, + bytes32 salt, + bytes memory initCode, + uint16 expectedBitmap + ) internal returns (address deployed) { + deployed = createX.deployCreate3(salt, initCode); + + // Verify the deployed address has the expected bitmap + uint16 actualBitmap = EffectBitmap.extractBitmap(deployed); + if (actualBitmap != expectedBitmap) { + revert BitmapMismatch(deployed, expectedBitmap, actualBitmap); + } + } + + /// @notice Deploy an effect without bitmap verification (use with caution) + /// @param createX The CreateX factory contract + /// @param salt The salt for CREATE3 deployment + /// @param initCode The contract creation bytecode + /// @return deployed The deployed contract address + function deployUnchecked( + CreateX createX, + bytes32 salt, + bytes memory initCode + ) internal returns (address deployed) { + deployed = createX.deployCreate3(salt, initCode); + } + + /// @notice Compute the address that would be deployed with a given salt + /// @param createX The CreateX factory contract + /// @param salt The salt for CREATE3 deployment + /// @return The computed address + function computeAddress(CreateX createX, bytes32 salt) internal view returns (address) { + return createX.computeCreate3Address(salt); + } + + /// @notice Compute the bitmap for a given CREATE3 salt + /// @param createX The CreateX factory contract + /// @param salt The salt for CREATE3 deployment + /// @return The bitmap that the deployed address would have + function computeBitmap(CreateX createX, bytes32 salt) internal view returns (uint16) { + address addr = createX.computeCreate3Address(salt); + return EffectBitmap.extractBitmap(addr); + } +} diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index 6a26ca29..80940fe3 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {DEFAULT_PRIORITY} from "../../Constants.sol"; import {EffectStep, ExtraDataType, MoveClass, Type, MonStateIndexName} from "../../Enums.sol"; -import {EffectInstance} from "../../Structs.sol"; +import {EffectContext, EffectInstance} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; @@ -82,7 +82,7 @@ contract IronWall is IMoveSet, BasicEffect { return (step == EffectStep.AfterDamage || step == EffectStep.OnMonSwitchOut); } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32 damageDealt) + function onAfterDamage(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32 damageDealt) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -93,7 +93,7 @@ contract IronWall is IMoveSet, BasicEffect { if ( healAmount > 0 && ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.IsKnockedOut + ctx.battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut ) == 0 ) { ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, healAmount); @@ -101,7 +101,7 @@ contract IronWall is IMoveSet, BasicEffect { return (extraData, false); } - function onMonSwitchOut(uint256, bytes32, uint256, uint256) + function onMonSwitchOut(EffectContext calldata, uint256, bytes32, uint256, uint256) external pure override diff --git a/src/mons/aurox/UpOnly.sol b/src/mons/aurox/UpOnly.sol index 628cee21..46516c0b 100644 --- a/src/mons/aurox/UpOnly.sol +++ b/src/mons/aurox/UpOnly.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {EffectStep} from "../../Enums.sol"; import {MonStateIndexName, StatBoostType, StatBoostFlag} from "../../Enums.sol"; -import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; +import {EffectContext, EffectInstance, StatBoostToApply} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -44,7 +44,7 @@ contract UpOnly is IAbility, BasicEffect { return (step == EffectStep.AfterDamage); } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(EffectContext calldata, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/mons/embursa/Q5.sol b/src/mons/embursa/Q5.sol index bce6d491..ad77cc3a 100644 --- a/src/mons/embursa/Q5.sol +++ b/src/mons/embursa/Q5.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "../../Constants.sol"; import "../../Enums.sol"; +import {EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -78,7 +79,7 @@ contract Q5 is IMoveSet, BasicEffect { return (step == EffectStep.RoundStart); } - function onRoundStart(uint256 rng, bytes32 extraData, uint256, uint256) + function onRoundStart(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256, uint256) external override returns (bytes32, bool) @@ -89,13 +90,13 @@ contract Q5 is IMoveSet, BasicEffect { AttackCalculator._calculateDamage( ENGINE, TYPE_CALCULATOR, - ENGINE.battleKeyForWrite(), + ctx.battleKey, attackerPlayerIndex, BASE_POWER, DEFAULT_ACCURACY, DEFAULT_VOL, - moveType(ENGINE.battleKeyForWrite()), - moveClass(ENGINE.battleKeyForWrite()), + moveType(ctx.battleKey), + moveClass(ctx.battleKey), rng, DEFAULT_CRIT_RATE ); diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index 94ededce..76639e55 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import {NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, MOVE_INDEX_MASK} from "../../Constants.sol"; import {EffectStep, MonStateIndexName, StatBoostFlag, StatBoostType} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {EffectInstance, IEffect, MoveDecision, StatBoostToApply} from "../../Structs.sol"; +import {EffectContext, EffectInstance, IEffect, MoveDecision, StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {StatBoosts} from "../../effects/StatBoosts.sol"; @@ -45,12 +45,12 @@ contract Tinderclaws is IAbility, BasicEffect { } // extraData: 0 = no SpATK boost applied, 1 = SpATK boost applied - function onAfterMove(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onAfterMove(EffectContext calldata ctx, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + bytes32 battleKey = ctx.battleKey; MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); // Unpack the move index from packedMoveIndex uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; @@ -74,12 +74,12 @@ contract Tinderclaws is IAbility, BasicEffect { return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + bytes32 battleKey = ctx.battleKey; bool isBurned = _isBurned(battleKey, targetIndex, monIndex); bool hasBoost = uint256(extraData) == 1; diff --git a/src/mons/ghouliath/RiseFromTheGrave.sol b/src/mons/ghouliath/RiseFromTheGrave.sol index a34bafcd..e0f78007 100644 --- a/src/mons/ghouliath/RiseFromTheGrave.sol +++ b/src/mons/ghouliath/RiseFromTheGrave.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import {EffectStep} from "../../Enums.sol"; import {MonStateIndexName} from "../../Enums.sol"; +import {EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -47,7 +48,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { return (step == EffectStep.RoundEnd || step == EffectStep.AfterDamage); } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -59,7 +60,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { // If the mon is KO'd, add this effect to the global effects list and remove the mon effect if ( ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.IsKnockedOut + ctx.battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut ) == 1 ) { uint64 v1 = REVIVAL_DELAY; @@ -73,7 +74,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { } // Regain stamina on round end, this can overheal stamina - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -92,9 +93,8 @@ contract RiseFromTheGrave is IAbility, BasicEffect { else if (turnsLeft == 1) { // Revive the mon and set HP to 1 ENGINE.updateMonState(playerIndex, monIndex, MonStateIndexName.IsKnockedOut, 0); - bytes32 battleKey = ENGINE.battleKeyForWrite(); - int32 currentDamage = ENGINE.getMonStateForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); - uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + int32 currentDamage = ENGINE.getMonStateForBattle(ctx.battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + uint32 maxHp = ENGINE.getMonValueForBattle(ctx.battleKey, playerIndex, monIndex, MonStateIndexName.Hp); int32 hpShiftAmount = 1 - currentDamage - int32(maxHp); ENGINE.updateMonState(playerIndex, monIndex, MonStateIndexName.Hp, hpShiftAmount); diff --git a/src/mons/gorillax/Angery.sol b/src/mons/gorillax/Angery.sol index 5406a3c6..04e6b235 100644 --- a/src/mons/gorillax/Angery.sol +++ b/src/mons/gorillax/Angery.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import {EffectStep} from "../../Enums.sol"; import {MonStateIndexName} from "../../Enums.sol"; -import {EffectInstance} from "../../Structs.sol"; +import {EffectContext, EffectInstance} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; @@ -43,7 +43,7 @@ contract Angery is IAbility, BasicEffect { return (step == EffectStep.RoundEnd || step == EffectStep.AfterDamage); } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -53,7 +53,7 @@ contract Angery is IAbility, BasicEffect { // Heal int32 healAmount = int32( - ENGINE.getMonValueForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp) + ENGINE.getMonValueForBattle(ctx.battleKey, targetIndex, monIndex, MonStateIndexName.Hp) ) / MAX_HP_DENOM; ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, healAmount); // Reset the charges @@ -63,7 +63,7 @@ contract Angery is IAbility, BasicEffect { } } - function onAfterDamage(uint256, bytes32 extraData, uint256, uint256, int32) + function onAfterDamage(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256, int32) external pure override diff --git a/src/mons/iblivion/Baselight.sol b/src/mons/iblivion/Baselight.sol index f757aa2f..881f87f0 100644 --- a/src/mons/iblivion/Baselight.sol +++ b/src/mons/iblivion/Baselight.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {EffectStep} from "../../Enums.sol"; -import {EffectInstance} from "../../Structs.sol"; +import {EffectContext, EffectInstance} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -87,7 +87,7 @@ contract Baselight is IAbility, BasicEffect { return (step == EffectStep.RoundEnd); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external pure override diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index 631f0cfe..b273ff91 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "../../Constants.sol"; import "../../Enums.sol"; -import "../../Structs.sol"; +import {EffectContext, EffectInstance} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -83,12 +83,12 @@ contract ChainExpansion is IMoveSet, BasicEffect { return (step == EffectStep.OnMonSwitchIn); } - function onMonSwitchIn(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + bytes32 battleKey = ctx.battleKey; (uint256 chargesLeft, uint256 ownerIndex) = _decodeState(extraData); // If it's a friendly mon, then we heal (flat 1/8 of max HP) if (targetIndex == ownerIndex) { diff --git a/src/mons/inutia/Initialize.sol b/src/mons/inutia/Initialize.sol index de3da338..3f6d7a10 100644 --- a/src/mons/inutia/Initialize.sol +++ b/src/mons/inutia/Initialize.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "../../Constants.sol"; import "../../Enums.sol"; -import {StatBoostToApply} from "../../Structs.sol"; +import {EffectContext, StatBoostToApply} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -98,7 +98,7 @@ contract Initialize is IMoveSet, BasicEffect { return (step == EffectStep.OnMonSwitchIn || step == EffectStep.OnMonSwitchOut); } - function onMonSwitchOut(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(EffectContext calldata, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -111,7 +111,7 @@ contract Initialize is IMoveSet, BasicEffect { return (extraData, false); } - function onMonSwitchIn(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(EffectContext calldata, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/mons/inutia/Interweaving.sol b/src/mons/inutia/Interweaving.sol index c22915d8..5360f66d 100644 --- a/src/mons/inutia/Interweaving.sol +++ b/src/mons/inutia/Interweaving.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "../../Enums.sol"; -import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; +import {EffectContext, EffectInstance, StatBoostToApply} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -53,14 +53,14 @@ contract Interweaving is IAbility, BasicEffect { return (step == EffectStep.OnMonSwitchOut || step == EffectStep.OnApply); } - function onMonSwitchOut(uint256, bytes32, uint256 targetIndex, uint256) + function onMonSwitchOut(EffectContext calldata ctx, uint256, bytes32, uint256 targetIndex, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { uint256 otherPlayerIndex = (targetIndex + 1) % 2; uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + ENGINE.getActiveMonIndexForBattleState(ctx.battleKey)[otherPlayerIndex]; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.SpecialAttack, diff --git a/src/mons/malalien/ActusReus.sol b/src/mons/malalien/ActusReus.sol index 2614b547..51823028 100644 --- a/src/mons/malalien/ActusReus.sol +++ b/src/mons/malalien/ActusReus.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {EffectStep, MonStateIndexName, StatBoostType, StatBoostFlag} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; +import {EffectContext, EffectInstance, StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; @@ -42,7 +42,7 @@ contract ActusReus is IAbility, BasicEffect { return (step == EffectStep.AfterMove || step == EffectStep.AfterDamage); } - function onAfterMove(uint256, bytes32 extraData, uint256 targetIndex, uint256) + function onAfterMove(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256) external override view @@ -51,10 +51,10 @@ contract ActusReus is IAbility, BasicEffect { // Check if opposing mon is KOed uint256 otherPlayerIndex = (targetIndex + 1) % 2; uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + ENGINE.getActiveMonIndexForBattleState(ctx.battleKey)[otherPlayerIndex]; bool isOtherMonKOed = ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), otherPlayerIndex, otherPlayerActiveMonIndex, MonStateIndexName.IsKnockedOut + ctx.battleKey, otherPlayerIndex, otherPlayerActiveMonIndex, MonStateIndexName.IsKnockedOut ) == 1; if (isOtherMonKOed) { return (bytes32(uint256(1)), false); @@ -62,7 +62,7 @@ contract ActusReus is IAbility, BasicEffect { return (extraData, false); } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) external override returns (bytes32, bool) @@ -72,12 +72,12 @@ contract ActusReus is IAbility, BasicEffect { // If we are KO'ed, set a speed delta of half of the opposing mon's base speed bool isKOed = ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.IsKnockedOut + ctx.battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut ) == 1; if (isKOed) { uint256 otherPlayerIndex = (targetIndex + 1) % 2; uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + ENGINE.getActiveMonIndexForBattleState(ctx.battleKey)[otherPlayerIndex]; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.Speed, diff --git a/src/mons/pengym/PostWorkout.sol b/src/mons/pengym/PostWorkout.sol index fa0b986a..b24a003a 100644 --- a/src/mons/pengym/PostWorkout.sol +++ b/src/mons/pengym/PostWorkout.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {EffectStep, MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {EffectInstance} from "../../Structs.sol"; +import {EffectContext, EffectInstance} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; @@ -37,12 +37,12 @@ contract PostWorkout is IAbility, BasicEffect { return (step == EffectStep.OnMonSwitchOut); } - function onMonSwitchOut(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(EffectContext calldata ctx, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + bytes32 battleKey = ctx.battleKey; bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); uint192 statusAddress = ENGINE.getGlobalKV(battleKey, keyForMon); diff --git a/src/mons/sofabbi/CarrotHarvest.sol b/src/mons/sofabbi/CarrotHarvest.sol index 73db36a9..e58232ab 100644 --- a/src/mons/sofabbi/CarrotHarvest.sol +++ b/src/mons/sofabbi/CarrotHarvest.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import {EffectStep} from "../../Enums.sol"; import {MonStateIndexName} from "../../Enums.sol"; -import {EffectInstance} from "../../Structs.sol"; +import {EffectContext, EffectInstance} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; @@ -43,7 +43,7 @@ contract CarrotHarvest is IAbility, BasicEffect { } // Regain stamina on round end, this can overheal stamina - function onRoundEnd(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/mons/xmon/Dreamcatcher.sol b/src/mons/xmon/Dreamcatcher.sol index 72596eac..cd5052ff 100644 --- a/src/mons/xmon/Dreamcatcher.sol +++ b/src/mons/xmon/Dreamcatcher.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "../../Enums.sol"; -import {MonStateIndexName, EffectInstance} from "../../Structs.sol"; +import {MonStateIndexName, EffectInstance, EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; @@ -40,6 +40,7 @@ contract Dreamcatcher is IAbility, BasicEffect { } function onUpdateMonState( + EffectContext calldata ctx, uint256, bytes32 extraData, uint256 playerIndex, @@ -49,12 +50,11 @@ contract Dreamcatcher is IAbility, BasicEffect { ) external override returns (bytes32, bool) { // Only trigger if Stamina is being increased (positive valueToAdd) if (stateVarIndex == MonStateIndexName.Stamina && valueToAdd > 0) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); - uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + uint32 maxHp = ENGINE.getMonValueForBattle(ctx.battleKey, playerIndex, monIndex, MonStateIndexName.Hp); int32 healAmount = int32(uint32(maxHp)) / HEAL_DENOM; // Prevent overhealing - int32 existingHpDelta = ENGINE.getMonStateForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + int32 existingHpDelta = ENGINE.getMonStateForBattle(ctx.battleKey, playerIndex, monIndex, MonStateIndexName.Hp); if (existingHpDelta + healAmount > 0) { healAmount = 0 - existingHpDelta; } diff --git a/src/mons/xmon/NightTerrors.sol b/src/mons/xmon/NightTerrors.sol index 31c6c528..a2953d2f 100644 --- a/src/mons/xmon/NightTerrors.sol +++ b/src/mons/xmon/NightTerrors.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {DEFAULT_PRIORITY, DEFAULT_ACCURACY, DEFAULT_VOL, DEFAULT_CRIT_RATE} from "../../Constants.sol"; import {EffectStep, ExtraDataType, MoveClass, Type, MonStateIndexName} from "../../Enums.sol"; -import {EffectInstance} from "../../Structs.sol"; +import {EffectInstance, EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {ITypeCalculator} from "../../types/ITypeCalculator.sol"; @@ -104,7 +104,7 @@ contract NightTerrors is IMoveSet, BasicEffect { return (step == EffectStep.RoundEnd || step == EffectStep.OnMonSwitchOut); } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) @@ -113,7 +113,7 @@ contract NightTerrors is IMoveSet, BasicEffect { // defenderPlayerIndex is stored in extraData (who should take damage) (uint64 defenderPlayerIndex, uint64 terrorCount) = _unpackExtraData(extraData); - bytes32 battleKey = ENGINE.battleKeyForWrite(); + bytes32 battleKey = ctx.battleKey; // Check current stamina of the attacker (who has the effect) int32 staminaDelta = ENGINE.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); @@ -164,7 +164,7 @@ contract NightTerrors is IMoveSet, BasicEffect { return (extraData, false); } - function onMonSwitchOut(uint256, bytes32 extraData, uint256, uint256) + function onMonSwitchOut(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external pure override diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index 89a4f758..b5649a93 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {NO_OP_MOVE_INDEX, DEFAULT_PRIORITY, MOVE_INDEX_MASK} from "../../Constants.sol"; import {EffectStep, ExtraDataType, MoveClass, Type} from "../../Enums.sol"; -import {MoveDecision, MonStateIndexName, EffectInstance} from "../../Structs.sol"; +import {MoveDecision, MonStateIndexName, EffectInstance, EffectContext} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {IMoveSet} from "../../moves/IMoveSet.sol"; @@ -64,12 +64,12 @@ contract Somniphobia is IMoveSet, BasicEffect { return (step == EffectStep.AfterMove || step == EffectStep.RoundEnd); } - function onAfterMove(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onAfterMove(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + bytes32 battleKey = ctx.battleKey; MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); // Unpack the move index from packedMoveIndex @@ -88,7 +88,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(EffectContext calldata, uint256, bytes32 extraData, uint256, uint256) external pure override diff --git a/test/EngineGasTest.sol b/test/EngineGasTest.sol index e630f8d4..846124e7 100644 --- a/test/EngineGasTest.sol +++ b/test/EngineGasTest.sol @@ -38,8 +38,9 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; +import {EffectTestHelper} from "./abstract/EffectTestHelper.sol"; -contract EngineGasTest is Test, BattleHelper { +contract EngineGasTest is Test, BattleHelper, EffectTestHelper { DefaultCommitManager commitManager; Engine engine; @@ -94,9 +95,11 @@ contract EngineGasTest is Test, BattleHelper { mon.stats.specialAttack = 10; mon.moves = new IMoveSet[](4); - StatBoosts statBoosts = new StatBoosts(engine); - IMoveSet burnMove = new EffectAttack(engine, new BurnStatus(engine, statBoosts), EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); - IMoveSet frostbiteMove = new EffectAttack(engine, new FrostbiteStatus(engine, statBoosts), EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); + StatBoosts statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(engine))); + BurnStatus burnStatus = BurnStatus(deployWithCorrectBitmap(new BurnStatus(engine, statBoosts))); + FrostbiteStatus frostbiteStatus = FrostbiteStatus(deployWithCorrectBitmap(new FrostbiteStatus(engine, statBoosts))); + IMoveSet burnMove = new EffectAttack(engine, burnStatus, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); + IMoveSet frostbiteMove = new EffectAttack(engine, frostbiteStatus, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); IMoveSet statBoostMove = new StatBoostsMove(engine, statBoosts); IMoveSet damageMove = new CustomAttack(engine, ITypeCalculator(address(typeCalc)), CustomAttack.Args({TYPE: Type.Fire, BASE_POWER: 10, ACCURACY: 100, STAMINA_COST: 1, PRIORITY: 1})); mon.moves[0] = burnMove; @@ -113,7 +116,7 @@ contract EngineGasTest is Test, BattleHelper { DefaultValidator validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: team.length, MOVES_PER_MON: mon.moves.length, TIMEOUT_DURATION: 10}) ); - StaminaRegen staminaRegen = new StaminaRegen(engine); + StaminaRegen staminaRegen = StaminaRegen(deployWithCorrectBitmap(new StaminaRegen(engine))); IEffect[] memory effects = new IEffect[](1); effects[0] = staminaRegen; DefaultRuleset ruleset = new DefaultRuleset(IEngine(address(engine)), effects); @@ -420,7 +423,7 @@ contract EngineGasTest is Test, BattleHelper { }); // Move that applies a status effect to opponent (no damage) - SingleInstanceEffect testEffect = new SingleInstanceEffect(engine); + SingleInstanceEffect testEffect = SingleInstanceEffect(deployWithCorrectBitmap(new SingleInstanceEffect(engine))); EffectAttack effectMove = new EffectAttack(engine, IEffect(address(testEffect)), EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 0, PRIORITY: 3})); // Damage move - high power to guarantee KO @@ -442,9 +445,9 @@ contract EngineGasTest is Test, BattleHelper { ); // Use ruleset with StaminaRegen effect - StaminaRegen staminaRegen = new StaminaRegen(engine); + StaminaRegen staminaRegenEffect = StaminaRegen(deployWithCorrectBitmap(new StaminaRegen(engine))); IEffect[] memory effects = new IEffect[](1); - effects[0] = staminaRegen; + effects[0] = staminaRegenEffect; IRuleset rulesetWithEffect = IRuleset(address(new DefaultRuleset(engine, effects))); // Battle 1: Fresh storage diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 7e270d8e..05547cff 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -43,11 +43,12 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; +import {EffectTestHelper} from "./abstract/EffectTestHelper.sol"; import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; import {EditEffectAttack} from "./mocks/EditEffectAttack.sol"; import {DummyStatus} from "./mocks/DummyStatus.sol"; -contract EngineTest is Test, BattleHelper { +contract EngineTest is Test, BattleHelper, EffectTestHelper { DefaultCommitManager commitManager; Engine engine; DefaultValidator validator; @@ -695,7 +696,7 @@ contract EngineTest is Test, BattleHelper { DefaultValidator twoMonValidator = new DefaultValidator( engine, DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: TIMEOUT_DURATION}) ); - StaminaRegen regen = new StaminaRegen(engine); + StaminaRegen regen = StaminaRegen(deployWithCorrectBitmap(new StaminaRegen(engine))); IEffect[] memory effects = new IEffect[](1); effects[0] = regen; DefaultRuleset rules = new DefaultRuleset(engine, effects); @@ -922,7 +923,7 @@ contract EngineTest is Test, BattleHelper { ability: IAbility(address(0)) }); // Instant death attack - IEffect instantDeath = new InstantDeathEffect(engine); + IEffect instantDeath = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IMoveSet instantDeathAttack = new EffectAttack(engine, instantDeath, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); IMoveSet[] memory deathMoves = new IMoveSet[](1); @@ -998,7 +999,7 @@ contract EngineTest is Test, BattleHelper { ability: IAbility(address(0)) }); // Instant death attack - IEffect instantDeath = new InstantDeathEffect(engine); + IEffect instantDeath = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IMoveSet instantDeathAttack = new EffectAttack(engine, instantDeath, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); IMoveSet[] memory deathMoves = new IMoveSet[](1); @@ -1061,7 +1062,7 @@ contract EngineTest is Test, BattleHelper { function test_effectAppliedByAttackCorrectlyAppliesToTargetedMonEvenAfterSwitch() public { // Mon that has a temporary stat boost effect - IEffect statBoost = new OneTurnStatBoost(engine); + IEffect statBoost = IEffect(deployWithCorrectBitmap(new OneTurnStatBoost(engine))); IMoveSet[] memory moves = new IMoveSet[](1); // Create new effect attack that applies the temporary stat boost effect @@ -1136,7 +1137,7 @@ contract EngineTest is Test, BattleHelper { ability: IAbility(address(0)) }); // Instant death attack - IEffect instantDeath = new InstantDeathEffect(engine); + IEffect instantDeath = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IMoveSet instantDeathAttack = new EffectAttack(engine, instantDeath, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); IMoveSet[] memory deathMoves = new IMoveSet[](1); @@ -1213,7 +1214,7 @@ contract EngineTest is Test, BattleHelper { ability: IAbility(address(0)) }); // Instant death attack - IEffect instantDeath = new InstantDeathEffect(engine); + IEffect instantDeath = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IMoveSet instantDeathAttack = new EffectAttack(engine, instantDeath, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); IMoveSet[] memory deathMoves = new IMoveSet[](1); @@ -1307,7 +1308,7 @@ contract EngineTest is Test, BattleHelper { ability: IAbility(address(0)) }); // Instant death attack - IEffect instantDeath = new InstantDeathEffect(engine); + IEffect instantDeath = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IMoveSet instantDeathAttack = new EffectAttack(engine, instantDeath, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); IMoveSet[] memory deathMoves = new IMoveSet[](1); @@ -1810,7 +1811,7 @@ contract EngineTest is Test, BattleHelper { }); // Create a new GlobalEffectAttack that applies InstantDeathOnSwitchIn - IEffect instantDeathOnSwitchIn = new InstantDeathOnSwitchInEffect(engine); + IEffect instantDeathOnSwitchIn = IEffect(deployWithCorrectBitmap(new InstantDeathOnSwitchInEffect(engine))); // Move should be higher priority than the switch attack IMoveSet instantDeathOnSwitchInAttack = new GlobalEffectAttack( @@ -1878,7 +1879,7 @@ contract EngineTest is Test, BattleHelper { // Initialize mons and moves IMoveSet switchAttack = new ForceSwitchMove(engine, ForceSwitchMove.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); - IEffect instantDeathOnSwitchIn = new InstantDeathOnSwitchInEffect(engine); + IEffect instantDeathOnSwitchIn = IEffect(deployWithCorrectBitmap(new InstantDeathOnSwitchInEffect(engine))); IMoveSet instantDeathOnSwitchInAttack = new GlobalEffectAttack( engine, instantDeathOnSwitchIn, GlobalEffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 2}) ); @@ -1946,7 +1947,7 @@ contract EngineTest is Test, BattleHelper { // environmental effect kills mon after switch in move (not as a side effect from move) function test_effectOnSwitchInFromDirectSwitchMoveKOsAndForcesSwitch() public { // Initialize mons and moves - IEffect instantDeathOnSwitchIn = new InstantDeathOnSwitchInEffect(engine); + IEffect instantDeathOnSwitchIn = IEffect(deployWithCorrectBitmap(new InstantDeathOnSwitchInEffect(engine))); // Set priority to be higher than switch IMoveSet instantDeathOnSwitchInAttack = new GlobalEffectAttack( @@ -2015,7 +2016,7 @@ contract EngineTest is Test, BattleHelper { function test_abilityOnSwitchInKOsAndLeadsToGameOver() public { // Initialize mons and moves IMoveSet[] memory moves = new IMoveSet[](0); - IEffect instantDeathAtEndOfTurn = new InstantDeathEffect(engine); + IEffect instantDeathAtEndOfTurn = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IAbility suicideAbility = new EffectAbility(engine, instantDeathAtEndOfTurn); Mon memory suicideMon = Mon({ stats: MonStats({ @@ -2100,7 +2101,7 @@ contract EngineTest is Test, BattleHelper { moves: switchMoves, ability: IAbility(address(0)) }); - IEffect instantDeathAtEndOfTurn = new InstantDeathEffect(engine); + IEffect instantDeathAtEndOfTurn = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IAbility suicideAbility = new EffectAbility(engine, instantDeathAtEndOfTurn); Mon memory suicideMon = Mon({ stats: MonStats({ @@ -2201,7 +2202,7 @@ contract EngineTest is Test, BattleHelper { moves: switchMoves, ability: IAbility(address(0)) }); - IEffect instantDeathAtEndOfTurn = new InstantDeathEffect(engine); + IEffect instantDeathAtEndOfTurn = IEffect(deployWithCorrectBitmap(new InstantDeathEffect(engine))); IAbility suicideAbility = new EffectAbility(engine, instantDeathAtEndOfTurn); Mon memory suicideMon = Mon({ stats: MonStats({ @@ -2284,7 +2285,7 @@ contract EngineTest is Test, BattleHelper { // attack that applies effect can only apply once (checks using an effect that writes to global KV) function test_attackThatAppliesEffectCanOnlyApplyOnce() public { // Single instance effect - IEffect singleInstanceEffect = new SingleInstanceEffect(engine); + IEffect singleInstanceEffect = IEffect(deployWithCorrectBitmap(new SingleInstanceEffect(engine))); IMoveSet effectAttack = new EffectAttack( engine, singleInstanceEffect, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1}) ); @@ -2386,7 +2387,7 @@ contract EngineTest is Test, BattleHelper { function test_onMonSwitchOutHookWorksWithTempStatBoost() public { // Mon that has a temporary stat boost effect - IEffect temporaryStatBoostEffect = new TempStatBoostEffect(engine); + IEffect temporaryStatBoostEffect = IEffect(deployWithCorrectBitmap(new TempStatBoostEffect(engine))); IMoveSet[] memory moves = new IMoveSet[](1); // Create new effect attack that applies the temporary stat boost effect @@ -2447,7 +2448,7 @@ contract EngineTest is Test, BattleHelper { function test_afterDamageHookRuns() public { // Create an attack that adds the rebound effect to the caller - IEffect reboundEffect = new AfterDamageReboundEffect(engine); + IEffect reboundEffect = IEffect(deployWithCorrectBitmap(new AfterDamageReboundEffect(engine))); IMoveSet[] memory moves = new IMoveSet[](2); IMoveSet reboundAttack = new EffectAttack(engine, reboundEffect, EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); @@ -3092,7 +3093,7 @@ contract EngineTest is Test, BattleHelper { function test_editEffect() public { EditEffectAttack editEffectAttack = new EditEffectAttack(engine); - DummyStatus d = new DummyStatus(); + DummyStatus d = DummyStatus(deployWithCorrectBitmap(new DummyStatus())); EffectAbility effectAbility = new EffectAbility(engine, d); Mon memory mon = _createMon(); mon.ability = effectAbility; diff --git a/test/abstract/EffectTestHelper.sol b/test/abstract/EffectTestHelper.sol new file mode 100644 index 00000000..bf8c88b5 --- /dev/null +++ b/test/abstract/EffectTestHelper.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {EffectStep} from "../../src/Enums.sol"; +import {EffectBitmap} from "../../src/lib/EffectBitmap.sol"; + +/// @title EffectTestHelper +/// @notice Helper for deploying effects at addresses with correct bitmaps in tests +/// @dev Uses vm.etch to place effect bytecode at addresses that have the correct +/// EffectStep bitmap encoded in their most significant bits. +/// +/// This is necessary because Engine.sol now uses EffectBitmap.shouldRunAtStep() +/// which reads the bitmap from the effect's address rather than calling the +/// effect's shouldRunAtStep() function. +/// +/// Usage: +/// StatBoosts statBoosts = new StatBoosts(engine); +/// statBoosts = StatBoosts(deployWithCorrectBitmap(statBoosts)); +abstract contract EffectTestHelper is Test { + /// @notice Deploy an effect at an address with the correct bitmap based on its shouldRunAtStep + /// @dev Queries the effect's shouldRunAtStep for all steps, builds the bitmap, and + /// copies the bytecode to an address with that bitmap encoded in its MSB. + /// @param effect The already-deployed effect contract + /// @return The effect at a new address with correct bitmap (cast to your desired type) + function deployWithCorrectBitmap(IEffect effect) internal returns (address) { + uint16 bitmap = _computeBitmapFromEffect(effect); + return deployWithBitmap(address(effect), bitmap); + } + + /// @notice Deploy any contract at an address with the specified bitmap + /// @dev Creates the contract normally, then copies its bytecode to a target address + /// that has the correct bitmap encoded in its MSB. + /// @param deployed The already-deployed contract address + /// @param bitmap The bitmap the contract should have + /// @return The new address with correct bitmap (cast to your desired type) + function deployWithBitmap(address deployed, uint16 bitmap) internal returns (address) { + // Compute a target address that has the correct bitmap + address targetAddr = _computeAddressWithBitmap(bitmap, uint256(uint160(deployed))); + + // Copy the bytecode to the target address + vm.etch(targetAddr, deployed.code); + + return targetAddr; + } + + /// @notice Compute the bitmap from an effect's shouldRunAtStep function + /// @param effect The effect to query + /// @return bitmap The computed bitmap + function _computeBitmapFromEffect(IEffect effect) internal returns (uint16 bitmap) { + uint256 numSteps = EffectBitmap.NUM_EFFECT_STEPS; + for (uint256 i = 0; i < numSteps; i++) { + if (effect.shouldRunAtStep(EffectStep(i))) { + // Bit position: MSB is step 0, LSB is step (numSteps-1) + bitmap |= uint16(1 << (numSteps - 1 - i)); + } + } + } + + /// @notice Compute an address that has the specified bitmap in its MSB + /// @param bitmap The desired bitmap (NUM_EFFECT_STEPS bits) + /// @param seed A seed value to make the address unique + /// @return An address with the bitmap encoded in its most significant bits + function _computeAddressWithBitmap(uint16 bitmap, uint256 seed) internal pure returns (address) { + // The bitmap is stored in the top NUM_EFFECT_STEPS bits of the address + // For NUM_EFFECT_STEPS=9, we need to place the 9-bit bitmap in bits 159-151 + // This means: address = (bitmap << 151) | (lower 151 bits) + + uint256 numSteps = EffectBitmap.NUM_EFFECT_STEPS; + uint256 bitmapShift = 160 - numSteps; + + // Use seed to generate the lower bits, but mask out the top bits + uint160 lowerBits = uint160(seed) & uint160((1 << bitmapShift) - 1); + + // Place bitmap in the top bits + uint160 topBits = uint160(bitmap) << bitmapShift; + + return address(topBits | lowerBits); + } +} diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index de587dbe..b2afdf99 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -21,6 +21,7 @@ import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; // Import effects import {DefaultRuleset} from "../../src/DefaultRuleset.sol"; @@ -42,7 +43,7 @@ import {OnUpdateMonStateHealEffect} from "../mocks/OnUpdateMonStateHealEffect.so import {EffectAbility} from "../mocks/EffectAbility.sol"; import {ReduceSpAtkMove} from "../mocks/ReduceSpAtkMove.sol"; -contract EffectTest is Test, BattleHelper { +contract EffectTest is Test, BattleHelper, EffectTestHelper { DefaultCommitManager commitManager; Engine engine; DefaultValidator oneMonOneMoveValidator; @@ -88,13 +89,14 @@ contract EffectTest is Test, BattleHelper { // Deploy StandardAttackFactory standardAttackFactory = new StandardAttackFactory(engine, typeCalc); - // Deploy all effects - statBoosts = new StatBoosts(engine); - frostbiteStatus = new FrostbiteStatus(engine, statBoosts); - sleepStatus = new SleepStatus(engine); - panicStatus = new PanicStatus(engine); - burnStatus = new BurnStatus(engine, statBoosts); - zapStatus = new ZapStatus(engine); + // Deploy all effects with correct bitmaps using EffectTestHelper + // The bitmap is automatically computed from each effect's shouldRunAtStep function + statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(engine))); + frostbiteStatus = FrostbiteStatus(deployWithCorrectBitmap(new FrostbiteStatus(engine, statBoosts))); + sleepStatus = SleepStatus(deployWithCorrectBitmap(new SleepStatus(engine))); + panicStatus = PanicStatus(deployWithCorrectBitmap(new PanicStatus(engine))); + burnStatus = BurnStatus(deployWithCorrectBitmap(new BurnStatus(engine, statBoosts))); + zapStatus = ZapStatus(deployWithCorrectBitmap(new ZapStatus(engine))); matchmaker = new DefaultMatchmaker(engine); } @@ -677,7 +679,7 @@ contract EffectTest is Test, BattleHelper { } function test_staminaRegen() public { - StaminaRegen regen = new StaminaRegen(engine); + StaminaRegen regen = StaminaRegen(deployWithCorrectBitmap(new StaminaRegen(engine))); IEffect[] memory effects = new IEffect[](1); effects[0] = regen; DefaultRuleset rules = new DefaultRuleset(engine, effects); @@ -748,7 +750,9 @@ contract EffectTest is Test, BattleHelper { function test_onUpdateMonStateHook() public { // Import the mock effect and move - OnUpdateMonStateHealEffect healEffect = new OnUpdateMonStateHealEffect(engine); + OnUpdateMonStateHealEffect healEffect = OnUpdateMonStateHealEffect( + deployWithCorrectBitmap(new OnUpdateMonStateHealEffect(engine)) + ); EffectAbility healAbility = new EffectAbility(engine, healEffect); ReduceSpAtkMove reduceSpAtkMove = new ReduceSpAtkMove(engine); diff --git a/test/effects/StatBoosts.t.sol b/test/effects/StatBoosts.t.sol index 16f5ca2e..8ba66790 100644 --- a/test/effects/StatBoosts.t.sol +++ b/test/effects/StatBoosts.t.sol @@ -24,12 +24,13 @@ import {StatBoostsMove} from "../mocks/StatBoostsMove.sol"; import {DefaultMatchmaker} from "../../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {SpAtkDebuffEffect} from "../mocks/SpAtkDebuffEffect.sol"; import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; -contract StatBoostsTest is Test, BattleHelper { +contract StatBoostsTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -55,8 +56,8 @@ contract StatBoostsTest is Test, BattleHelper { ); commitManager = new DefaultCommitManager(IEngine(address(engine))); - // Create the StatBoosts effect and move - statBoosts = new StatBoosts(IEngine(address(engine))); + // Create the StatBoosts effect with correct bitmap (auto-computed from shouldRunAtStep) + statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); statBoostMove = new StatBoostsMove(IEngine(address(engine)), statBoosts); matchmaker = new DefaultMatchmaker(engine); } @@ -353,7 +354,9 @@ contract StatBoostsTest is Test, BattleHelper { function test_permanentTempStatBoostInteraction() public { StandardAttackFactory attackFactory = new StandardAttackFactory(engine, typeCalc); - SpAtkDebuffEffect spAtkDebuff = new SpAtkDebuffEffect(engine, statBoosts); + SpAtkDebuffEffect spAtkDebuff = SpAtkDebuffEffect( + deployWithCorrectBitmap(new SpAtkDebuffEffect(engine, statBoosts)) + ); // Create teams with two mons each IMoveSet[] memory moves = new IMoveSet[](2); diff --git a/test/mocks/AfterDamageReboundEffect.sol b/test/mocks/AfterDamageReboundEffect.sol index 93f4b27d..616b2484 100644 --- a/test/mocks/AfterDamageReboundEffect.sol +++ b/test/mocks/AfterDamageReboundEffect.sol @@ -21,14 +21,14 @@ contract AfterDamageReboundEffect is BasicEffect { } // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(EffectContext calldata ctx, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) external override returns (bytes32, bool) { // Heals for all damage done int32 currentDamage = - ENGINE.getMonStateForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp); + ENGINE.getMonStateForBattle(ctx.battleKey, targetIndex, monIndex, MonStateIndexName.Hp); ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, currentDamage * -1); return (extraData, false); } diff --git a/test/mocks/InstantDeathEffect.sol b/test/mocks/InstantDeathEffect.sol index c4d1f6e4..f6d9223c 100644 --- a/test/mocks/InstantDeathEffect.sol +++ b/test/mocks/InstantDeathEffect.sol @@ -24,7 +24,7 @@ contract InstantDeathEffect is BasicEffect { return r == EffectStep.RoundEnd; } - function onRoundEnd(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/InstantDeathOnSwitchInEffect.sol b/test/mocks/InstantDeathOnSwitchInEffect.sol index a3d2df78..d42351a6 100644 --- a/test/mocks/InstantDeathOnSwitchInEffect.sol +++ b/test/mocks/InstantDeathOnSwitchInEffect.sol @@ -25,7 +25,7 @@ contract InstantDeathOnSwitchInEffect is BasicEffect { } // NOTE: ONLY RUN ON GLOBAL EFFECTS (mons have their Ability as their own hook to apply an effect on switch in) - function onMonSwitchIn(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/OnUpdateMonStateHealEffect.sol b/test/mocks/OnUpdateMonStateHealEffect.sol index aabe14b4..2c2cf9b6 100644 --- a/test/mocks/OnUpdateMonStateHealEffect.sol +++ b/test/mocks/OnUpdateMonStateHealEffect.sol @@ -28,6 +28,7 @@ contract OnUpdateMonStateHealEffect is BasicEffect { // WARNING: Avoid chaining this effect to prevent recursive calls // This effect is safe because it only heals HP, it doesn't trigger state updates that would recurse function onUpdateMonState( + EffectContext calldata, uint256, bytes32 extraData, uint256 playerIndex, diff --git a/test/mocks/OneTurnStatBoost.sol b/test/mocks/OneTurnStatBoost.sol index 46a24969..e293bf23 100644 --- a/test/mocks/OneTurnStatBoost.sol +++ b/test/mocks/OneTurnStatBoost.sol @@ -25,7 +25,7 @@ contract OneTurnStatBoost is BasicEffect { } // Adds a bonus - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -35,7 +35,7 @@ contract OneTurnStatBoost is BasicEffect { } // Adds another bonus - function onRoundEnd(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/SingleInstanceEffect.sol b/test/mocks/SingleInstanceEffect.sol index 7d657a68..df2bc943 100644 --- a/test/mocks/SingleInstanceEffect.sol +++ b/test/mocks/SingleInstanceEffect.sol @@ -23,7 +23,7 @@ contract SingleInstanceEffect is BasicEffect { return r == EffectStep.OnApply; } - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32, bool removeAfterRun) diff --git a/test/mocks/SpAtkDebuffEffect.sol b/test/mocks/SpAtkDebuffEffect.sol index 4472e11b..1f093e96 100644 --- a/test/mocks/SpAtkDebuffEffect.sol +++ b/test/mocks/SpAtkDebuffEffect.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {EffectStep, MonStateIndexName, StatBoostFlag, StatBoostType} from "../../src/Enums.sol"; import {IEngine} from "../../src/IEngine.sol"; -import {StatBoostToApply} from "../../src/Structs.sol"; +import {EffectContext, StatBoostToApply} from "../../src/Structs.sol"; import {StatusEffect} from "../../src/effects/status/StatusEffect.sol"; import {StatBoosts} from "../../src/effects/StatBoosts.sol"; @@ -25,7 +25,7 @@ contract SpAtkDebuffEffect is StatusEffect { return (r == EffectStep.OnApply || r == EffectStep.OnRemove); } - function onApply(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) public override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/TempStatBoostEffect.sol b/test/mocks/TempStatBoostEffect.sol index 6a787e77..18f2cabb 100644 --- a/test/mocks/TempStatBoostEffect.sol +++ b/test/mocks/TempStatBoostEffect.sol @@ -24,7 +24,7 @@ contract TempStatBoostEffect is BasicEffect { return (r == EffectStep.OnMonSwitchOut || r == EffectStep.OnApply); } - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -33,7 +33,7 @@ contract TempStatBoostEffect is BasicEffect { return (bytes32(0), false); } - function onMonSwitchOut(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(EffectContext calldata, uint256, bytes32, uint256 targetIndex, uint256 monIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mons/AuroxTest.sol b/test/mons/AuroxTest.sol index aa76e092..a74ae2a3 100644 --- a/test/mons/AuroxTest.sol +++ b/test/mons/AuroxTest.sol @@ -23,6 +23,7 @@ import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; @@ -47,7 +48,7 @@ import {VolatilePunch} from "../../src/mons/aurox/VolatilePunch.sol"; - rng of 10 should trigger burn */ -contract AuroxTest is Test, BattleHelper { +contract AuroxTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -63,7 +64,7 @@ contract AuroxTest is Test, BattleHelper { defaultRegistry = new TestTeamRegistry(); engine = new Engine(); commitManager = new DefaultCommitManager(IEngine(address(engine))); - statBoosts = new StatBoosts(IEngine(address(engine))); + statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); matchmaker = new DefaultMatchmaker(engine); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); } @@ -98,7 +99,7 @@ contract AuroxTest is Test, BattleHelper { } function test_gildedRecoveryHealsWithStatus() public { - FrostbiteStatus frostbiteStatus = new FrostbiteStatus(IEngine(address(engine)), statBoosts); + FrostbiteStatus frostbiteStatus = FrostbiteStatus(deployWithCorrectBitmap(new FrostbiteStatus(IEngine(address(engine)), statBoosts))); GildedRecovery gildedRecovery = new GildedRecovery(IEngine(address(engine))); uint32 maxHp = 100; @@ -250,7 +251,7 @@ contract AuroxTest is Test, BattleHelper { function test_ironWallHealsDamage() public { uint32 maxHp = 100; - IronWall ironWall = new IronWall(IEngine(address(engine))); + IronWall ironWall = IronWall(deployWithCorrectBitmap(new IronWall(IEngine(address(engine))))); StandardAttack attack = attackFactory.createAttack( ATTACK_PARAMS({ BASE_POWER: maxHp / 2, @@ -339,7 +340,7 @@ contract AuroxTest is Test, BattleHelper { function test_ironWallSkipsIfKO() public { uint32 maxHp = 100; - IronWall ironWall = new IronWall(IEngine(address(engine))); + IronWall ironWall = IronWall(deployWithCorrectBitmap(new IronWall(IEngine(address(engine))))); StandardAttack attack = attackFactory.createAttack( ATTACK_PARAMS({ BASE_POWER: maxHp, @@ -406,7 +407,7 @@ contract AuroxTest is Test, BattleHelper { function test_ironWallProvidesInitialHeal() public { uint32 maxHp = 100; - IronWall ironWall = new IronWall(IEngine(address(engine))); + IronWall ironWall = IronWall(deployWithCorrectBitmap(new IronWall(IEngine(address(engine))))); StandardAttack attack = attackFactory.createAttack( ATTACK_PARAMS({ BASE_POWER: maxHp / 2, @@ -481,7 +482,7 @@ contract AuroxTest is Test, BattleHelper { function test_ironWallDoesNothingIfAlreadyActive() public { uint32 maxHp = 100; - IronWall ironWall = new IronWall(IEngine(address(engine))); + IronWall ironWall = IronWall(deployWithCorrectBitmap(new IronWall(IEngine(address(engine))))); StandardAttack attack = attackFactory.createAttack( ATTACK_PARAMS({ BASE_POWER: maxHp / 2, @@ -574,7 +575,7 @@ contract AuroxTest is Test, BattleHelper { uint32 maxAtk = 100; uint32 maxDef = 100; - UpOnly upOnly = new UpOnly(IEngine(address(engine)), statBoosts); + UpOnly upOnly = UpOnly(deployWithCorrectBitmap(new UpOnly(IEngine(address(engine)), statBoosts))); StandardAttack attack = attackFactory.createAttack( ATTACK_PARAMS({ BASE_POWER: maxHp / 2, @@ -635,8 +636,8 @@ contract AuroxTest is Test, BattleHelper { function test_volatilePunchDealsDamageAndTriggersStatusEffects() public { uint32 maxHp = 100; - BurnStatus burnStatus = new BurnStatus(IEngine(address(engine)), statBoosts); - FrostbiteStatus frostbiteStatus = new FrostbiteStatus(IEngine(address(engine)), statBoosts); + BurnStatus burnStatus = BurnStatus(deployWithCorrectBitmap(new BurnStatus(IEngine(address(engine)), statBoosts))); + FrostbiteStatus frostbiteStatus = FrostbiteStatus(deployWithCorrectBitmap(new FrostbiteStatus(IEngine(address(engine)), statBoosts))); VolatilePunch volatilePunch = new VolatilePunch( IEngine(address(engine)), typeCalc, burnStatus, frostbiteStatus ); diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index 0fa3de9a..63760710 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -19,6 +19,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; @@ -37,7 +38,7 @@ import {SetAblaze} from "../../src/mons/embursa/SetAblaze.sol"; import {Tinderclaws} from "../../src/mons/embursa/Tinderclaws.sol"; import {DummyStatus} from "../mocks/DummyStatus.sol"; -contract EmbursaTest is Test, BattleHelper { +contract EmbursaTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -62,7 +63,7 @@ contract EmbursaTest is Test, BattleHelper { function test_q5() public { IMoveSet[] memory moves = new IMoveSet[](1); - moves[0] = new Q5(engine, typeCalc); + moves[0] = Q5(deployWithCorrectBitmap(new Q5(engine, typeCalc))); Mon memory mon = Mon({ stats: MonStats({ @@ -134,11 +135,11 @@ contract EmbursaTest is Test, BattleHelper { } function test_heatBeacon() public { - DummyStatus dummyStatus = new DummyStatus(); + DummyStatus dummyStatus = DummyStatus(deployWithCorrectBitmap(new DummyStatus())); HeatBeacon heatBeacon = new HeatBeacon(IEngine(address(engine)), IEffect(address(dummyStatus))); - Q5 q5 = new Q5(engine, typeCalc); + Q5 q5 = Q5(deployWithCorrectBitmap(new Q5(engine, typeCalc))); SetAblaze setAblaze = new SetAblaze(engine, typeCalc, IEffect(address(dummyStatus))); - StatBoosts statBoosts = new StatBoosts(engine); + StatBoosts statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(engine))); HoneyBribe honeyBribe = new HoneyBribe(engine, statBoosts); IMoveSet koMove = attackFactory.createAttack( @@ -318,9 +319,9 @@ contract EmbursaTest is Test, BattleHelper { * - If burn is applied externally, SpATK boost is still granted at end of round */ function test_tinderclaws_selfBurnOnMove() public { - StatBoosts statBoosts = new StatBoosts(engine); - BurnStatus burnStatus = new BurnStatus(IEngine(address(engine)), statBoosts); - Tinderclaws tinderclaws = new Tinderclaws(IEngine(address(engine)), IEffect(address(burnStatus)), statBoosts); + StatBoosts statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(engine))); + BurnStatus burnStatus = BurnStatus(deployWithCorrectBitmap(new BurnStatus(IEngine(address(engine)), statBoosts))); + Tinderclaws tinderclaws = Tinderclaws(deployWithCorrectBitmap(new Tinderclaws(IEngine(address(engine)), IEffect(address(burnStatus)), statBoosts))); IMoveSet[] memory moves = new IMoveSet[](1); moves[0] = attackFactory.createAttack( @@ -408,9 +409,9 @@ contract EmbursaTest is Test, BattleHelper { } function test_tinderclaws_restingRemovesBurn() public { - StatBoosts statBoosts = new StatBoosts(engine); - BurnStatus burnStatus = new BurnStatus(IEngine(address(engine)), statBoosts); - Tinderclaws tinderclaws = new Tinderclaws(IEngine(address(engine)), IEffect(address(burnStatus)), statBoosts); + StatBoosts statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(engine))); + BurnStatus burnStatus = BurnStatus(deployWithCorrectBitmap(new BurnStatus(IEngine(address(engine)), statBoosts))); + Tinderclaws tinderclaws = Tinderclaws(deployWithCorrectBitmap(new Tinderclaws(IEngine(address(engine)), IEffect(address(burnStatus)), statBoosts))); IMoveSet[] memory moves = new IMoveSet[](1); moves[0] = attackFactory.createAttack( diff --git a/test/mons/GhouliathTest.sol b/test/mons/GhouliathTest.sol index a9f486f0..82ce0d0b 100644 --- a/test/mons/GhouliathTest.sol +++ b/test/mons/GhouliathTest.sol @@ -25,6 +25,7 @@ import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {PanicStatus} from "../../src/effects/status/PanicStatus.sol"; import {DefaultMatchmaker} from "../../src/matchmaker/DefaultMatchmaker.sol"; @@ -33,7 +34,7 @@ import {Osteoporosis} from "../../src/mons/ghouliath/Osteoporosis.sol"; import {RiseFromTheGrave} from "../../src/mons/ghouliath/RiseFromTheGrave.sol"; import {WitherAway} from "../../src/mons/ghouliath/WitherAway.sol"; -contract GhouliathTest is Test, BattleHelper { +contract GhouliathTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -58,13 +59,13 @@ contract GhouliathTest is Test, BattleHelper { IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: 10}) ); commitManager = new DefaultCommitManager(IEngine(address(engine))); - riseFromTheGrave = new RiseFromTheGrave(IEngine(address(engine))); + riseFromTheGrave = RiseFromTheGrave(deployWithCorrectBitmap(new RiseFromTheGrave(IEngine(address(engine))))); osteoporosis = new Osteoporosis(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); - panicStatus = new PanicStatus(IEngine(address(engine))); + panicStatus = PanicStatus(deployWithCorrectBitmap(new PanicStatus(IEngine(address(engine))))); witherAway = new WitherAway(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), IEffect(address(panicStatus))); standardAttackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); - statBoosts = new StatBoosts(IEngine(address(engine))); + statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); eternalGrudge = new EternalGrudge(IEngine(address(engine)), statBoosts); matchmaker = new DefaultMatchmaker(engine); } diff --git a/test/mons/GorillaxTest.sol b/test/mons/GorillaxTest.sol index 5861d07b..d9f7da82 100644 --- a/test/mons/GorillaxTest.sol +++ b/test/mons/GorillaxTest.sol @@ -19,6 +19,7 @@ import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; @@ -27,7 +28,7 @@ import {DefaultMatchmaker} from "../../src/matchmaker/DefaultMatchmaker.sol"; import {Angery} from "../../src/mons/gorillax/Angery.sol"; import {RockPull} from "../../src/mons/gorillax/RockPull.sol"; -contract GorillaxTest is Test, BattleHelper { +contract GorillaxTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -50,7 +51,7 @@ contract GorillaxTest is Test, BattleHelper { DefaultValidator validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 1, TIMEOUT_DURATION: 10}) ); - Angery angery = new Angery(IEngine(address(engine))); + Angery angery = Angery(deployWithCorrectBitmap(new Angery(IEngine(address(engine))))); // Create a team with a mon that has Angery ability IMoveSet[] memory moves = new IMoveSet[](1); diff --git a/test/mons/IblivionTest.sol b/test/mons/IblivionTest.sol index 6fcf2f7a..d667d76c 100644 --- a/test/mons/IblivionTest.sol +++ b/test/mons/IblivionTest.sol @@ -17,6 +17,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; @@ -34,7 +35,7 @@ import {StandardAttack} from "../../src/moves/StandardAttack.sol"; import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {MockEffectRemover} from "../mocks/MockEffectRemover.sol"; -contract IblivionTest is Test, BattleHelper { +contract IblivionTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -61,12 +62,13 @@ contract IblivionTest is Test, BattleHelper { IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 4, TIMEOUT_DURATION: 10}) ); commitManager = new DefaultCommitManager(IEngine(address(engine))); - statBoost = new StatBoosts(IEngine(address(engine))); + // Deploy effects with correct bitmaps (auto-computed from shouldRunAtStep) + statBoost = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); matchmaker = new DefaultMatchmaker(engine); - // Deploy Iblivion contracts - baselight = new Baselight(IEngine(address(engine))); + // Deploy Iblivion contracts with correct bitmaps + baselight = Baselight(deployWithCorrectBitmap(new Baselight(IEngine(address(engine))))); brightback = new Brightback(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), baselight); unboundedStrike = new UnboundedStrike(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), baselight); loop = new Loop(IEngine(address(engine)), baselight, statBoost); diff --git a/test/mons/InutiaTest.sol b/test/mons/InutiaTest.sol index 496eab0f..85a46a17 100644 --- a/test/mons/InutiaTest.sol +++ b/test/mons/InutiaTest.sol @@ -20,6 +20,7 @@ import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {TypeCalculator} from "../../src/types/TypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; @@ -29,7 +30,7 @@ import {ChainExpansion} from "../../src/mons/inutia/ChainExpansion.sol"; import {Initialize} from "../../src/mons/inutia/Initialize.sol"; import {Interweaving} from "../../src/mons/inutia/Interweaving.sol"; -contract InutiaTest is Test, BattleHelper { +contract InutiaTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -46,8 +47,8 @@ contract InutiaTest is Test, BattleHelper { defaultRegistry = new TestTeamRegistry(); engine = new Engine(); commitManager = new DefaultCommitManager(IEngine(address(engine))); - statBoost = new StatBoosts(IEngine(address(engine))); - interweaving = new Interweaving(IEngine(address(engine)), statBoost); + statBoost = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); + interweaving = Interweaving(deployWithCorrectBitmap(new Interweaving(IEngine(address(engine)), statBoost))); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); matchmaker = new DefaultMatchmaker(engine); } @@ -141,7 +142,7 @@ contract InutiaTest is Test, BattleHelper { } function test_initialize() public { - Initialize initialize = new Initialize(engine, statBoost); + Initialize initialize = Initialize(deployWithCorrectBitmap(new Initialize(engine, statBoost))); // Create a validator with 2 mons and 1 move per mon DefaultValidator validator = new DefaultValidator( @@ -241,7 +242,7 @@ contract InutiaTest is Test, BattleHelper { function test_chainExpansion() public { TypeCalculator tc = new TypeCalculator(); - ChainExpansion ce = new ChainExpansion(engine, tc); + ChainExpansion ce = ChainExpansion(deployWithCorrectBitmap(new ChainExpansion(engine, tc))); DefaultValidator v = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 3, MOVES_PER_MON: 2, TIMEOUT_DURATION: 10}) ); diff --git a/test/mons/MalalienTest.sol b/test/mons/MalalienTest.sol index 8041aaca..873b90c8 100644 --- a/test/mons/MalalienTest.sol +++ b/test/mons/MalalienTest.sol @@ -18,6 +18,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; @@ -31,7 +32,7 @@ import {DefaultMatchmaker} from "../../src/matchmaker/DefaultMatchmaker.sol"; import {ActusReus} from "../../src/mons/malalien/ActusReus.sol"; import {TripleThink} from "../../src/mons/malalien/TripleThink.sol"; -contract MalalienTest is Test, BattleHelper { +contract MalalienTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -48,8 +49,8 @@ contract MalalienTest is Test, BattleHelper { defaultRegistry = new TestTeamRegistry(); engine = new Engine(); commitManager = new DefaultCommitManager(IEngine(address(engine))); - statBoosts = new StatBoosts(engine); - actusReus = new ActusReus(IEngine(address(engine)), statBoosts); + statBoosts = StatBoosts(deployWithCorrectBitmap(new StatBoosts(engine))); + actusReus = ActusReus(deployWithCorrectBitmap(new ActusReus(IEngine(address(engine)), statBoosts))); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); matchmaker = new DefaultMatchmaker(engine); } diff --git a/test/mons/PengymTest.sol b/test/mons/PengymTest.sol index cd783073..e8dc2037 100644 --- a/test/mons/PengymTest.sol +++ b/test/mons/PengymTest.sol @@ -22,6 +22,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; @@ -37,7 +38,7 @@ import {ChillOut} from "../../src/mons/pengym/ChillOut.sol"; import {DeepFreeze} from "../../src/mons/pengym/DeepFreeze.sol"; import {PistolSquat} from "../../src/mons/pengym/PistolSquat.sol"; -contract PengymTest is Test, BattleHelper { +contract PengymTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -61,10 +62,10 @@ contract PengymTest is Test, BattleHelper { ); commitManager = new DefaultCommitManager(IEngine(address(engine))); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); - postWorkout = new PostWorkout(IEngine(address(engine))); - panicStatus = new PanicStatus(IEngine(address(engine))); - statBoost = new StatBoosts(IEngine(address(engine))); - frostbiteStatus = new FrostbiteStatus(IEngine(address(engine)), statBoost); + postWorkout = PostWorkout(deployWithCorrectBitmap(new PostWorkout(IEngine(address(engine))))); + panicStatus = PanicStatus(deployWithCorrectBitmap(new PanicStatus(IEngine(address(engine))))); + statBoost = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); + frostbiteStatus = FrostbiteStatus(deployWithCorrectBitmap(new FrostbiteStatus(IEngine(address(engine)), statBoost))); matchmaker = new DefaultMatchmaker(engine); } diff --git a/test/mons/VolthareTest.sol b/test/mons/VolthareTest.sol index 0c5ce9ac..8643f182 100644 --- a/test/mons/VolthareTest.sol +++ b/test/mons/VolthareTest.sol @@ -18,6 +18,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; @@ -38,7 +39,7 @@ import {GlobalEffectAttack} from "../mocks/GlobalEffectAttack.sol"; import {DefaultMatchmaker} from "../../src/matchmaker/DefaultMatchmaker.sol"; import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; -contract VolthareTest is Test, BattleHelper { +contract VolthareTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -60,8 +61,8 @@ contract VolthareTest is Test, BattleHelper { IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 0, TIMEOUT_DURATION: 10}) ); commitManager = new DefaultCommitManager(IEngine(address(engine))); - statBoost = new StatBoosts(IEngine(address(engine))); - overclock = new Overclock(IEngine(address(engine)), statBoost); + statBoost = StatBoosts(deployWithCorrectBitmap(new StatBoosts(IEngine(address(engine))))); + overclock = Overclock(deployWithCorrectBitmap(new Overclock(IEngine(address(engine)), statBoost))); preemptiveShock = new PreemptiveShock(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); matchmaker = new DefaultMatchmaker(engine); @@ -145,7 +146,7 @@ contract VolthareTest is Test, BattleHelper { */ function test_megaStarBlast() public { // Create moves: one to apply Overclock, one is MegaStarBlast - DummyStatus zapStatus = new DummyStatus(); + DummyStatus zapStatus = DummyStatus(deployWithCorrectBitmap(new DummyStatus())); MegaStarBlast msb = new MegaStarBlast(engine, typeCalc, zapStatus, overclock); GlobalEffectAttack overclockMove = new GlobalEffectAttack( engine, @@ -254,7 +255,7 @@ contract VolthareTest is Test, BattleHelper { function test_dualShock() public { // Create a team with a mon that knows Dual Shock IMoveSet[] memory moves = new IMoveSet[](1); - ZapStatus zapStatus = new ZapStatus(engine); + ZapStatus zapStatus = ZapStatus(deployWithCorrectBitmap(new ZapStatus(engine))); DualShock dualShock = new DualShock(engine, typeCalc, zapStatus, overclock); moves[0] = IMoveSet(address(dualShock)); diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index 3f551bab..341d5232 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -25,6 +25,7 @@ import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; +import {EffectTestHelper} from "../abstract/EffectTestHelper.sol"; import {MockRandomnessOracle} from "../mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; @@ -46,7 +47,7 @@ import {NightTerrors} from "../../src/mons/xmon/NightTerrors.sol"; - Night Terrors damage differs when opponent is asleep vs awake [ ] */ -contract XmonTest is Test, BattleHelper { +contract XmonTest is Test, BattleHelper, EffectTestHelper { Engine engine; DefaultCommitManager commitManager; TestTypeCalculator typeCalc; @@ -66,7 +67,7 @@ contract XmonTest is Test, BattleHelper { } function test_contagiousSlumberAppliesSleepToBothMons() public { - SleepStatus sleepStatus = new SleepStatus(IEngine(address(engine))); + SleepStatus sleepStatus = SleepStatus(deployWithCorrectBitmap(new SleepStatus(IEngine(address(engine))))); ContagiousSlumber contagiousSlumber = new ContagiousSlumber(IEngine(address(engine)), IEffect(address(sleepStatus))); IMoveSet[] memory moves = new IMoveSet[](1); @@ -197,7 +198,7 @@ contract XmonTest is Test, BattleHelper { } function test_somniphobiaDamagesMonsWhoRest() public { - Somniphobia somniphobia = new Somniphobia(IEngine(address(engine))); + Somniphobia somniphobia = Somniphobia(deployWithCorrectBitmap(new Somniphobia(IEngine(address(engine))))); IMoveSet[] memory moves = new IMoveSet[](1); moves[0] = somniphobia; @@ -260,8 +261,8 @@ contract XmonTest is Test, BattleHelper { } function test_dreamcatcherHealsOnStaminaGain() public { - Dreamcatcher dreamcatcher = new Dreamcatcher(IEngine(address(engine))); - StaminaRegen staminaRegen = new StaminaRegen(IEngine(address(engine))); + Dreamcatcher dreamcatcher = Dreamcatcher(deployWithCorrectBitmap(new Dreamcatcher(IEngine(address(engine))))); + StaminaRegen staminaRegen = StaminaRegen(deployWithCorrectBitmap(new StaminaRegen(IEngine(address(engine))))); uint32 BASE_HP = 10; uint32 maxHp = uint32(dreamcatcher.HEAL_DENOM()) * BASE_HP; // 160 HP @@ -377,8 +378,8 @@ contract XmonTest is Test, BattleHelper { * Turn 2: Alice uses Night Terrors (2 stacks on Alice), Alice loses 2 stamina at end of turn (4 -> 2) * Turn 3: Alice uses Night Terrors (3 stacks on Alice), Alice has only 2 stamina, so no trigger */ - SleepStatus sleepStatus = new SleepStatus(IEngine(address(engine))); - NightTerrors nightTerrors = new NightTerrors(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), IEffect(address(sleepStatus))); + SleepStatus sleepStatus = SleepStatus(deployWithCorrectBitmap(new SleepStatus(IEngine(address(engine))))); + NightTerrors nightTerrors = NightTerrors(deployWithCorrectBitmap(new NightTerrors(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), IEffect(address(sleepStatus))))); IMoveSet[] memory moves = new IMoveSet[](1); moves[0] = nightTerrors; @@ -442,8 +443,8 @@ contract XmonTest is Test, BattleHelper { * Turn 2: Alice swaps to mon 1 * Verify: Alice's mon 0 no longer has Night Terrors effect */ - SleepStatus sleepStatus = new SleepStatus(IEngine(address(engine))); - NightTerrors nightTerrors = new NightTerrors(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), IEffect(address(sleepStatus))); + SleepStatus sleepStatus = SleepStatus(deployWithCorrectBitmap(new SleepStatus(IEngine(address(engine))))); + NightTerrors nightTerrors = NightTerrors(deployWithCorrectBitmap(new NightTerrors(IEngine(address(engine)), ITypeCalculator(address(typeCalc)), IEffect(address(sleepStatus))))); IMoveSet[] memory moves = new IMoveSet[](1); moves[0] = nightTerrors; diff --git a/tools/effect-miner/Cargo.lock b/tools/effect-miner/Cargo.lock new file mode 100644 index 00000000..32dece6c --- /dev/null +++ b/tools/effect-miner/Cargo.lock @@ -0,0 +1,413 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "effect-miner" +version = "0.1.0" +dependencies = [ + "clap", + "hex", + "rand", + "rayon", + "serde", + "serde_json", + "tiny-keccak", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/tools/effect-miner/Cargo.toml b/tools/effect-miner/Cargo.toml new file mode 100644 index 00000000..2fe1d5ce --- /dev/null +++ b/tools/effect-miner/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "effect-miner" +version = "0.1.0" +edition = "2021" +description = "Vanity address miner for Effect contracts using CREATE3" + +[dependencies] +tiny-keccak = { version = "2.0", features = ["keccak"] } +rayon = "1.10" +clap = { version = "4.5", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rand = "0.8" +hex = "0.4" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 diff --git a/tools/effect-miner/src/create3.rs b/tools/effect-miner/src/create3.rs new file mode 100644 index 00000000..2a971c07 --- /dev/null +++ b/tools/effect-miner/src/create3.rs @@ -0,0 +1,142 @@ +use tiny_keccak::{Hasher, Keccak}; + +/// Type aliases for clarity +pub type Address = [u8; 20]; +pub type B256 = [u8; 32]; + +/// The init code hash of the CREATE3 proxy used by CreateX +/// This is: keccak256(hex"67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3") +const PROXY_INIT_CODE_HASH: [u8; 32] = [ + 0x21, 0xc3, 0x5d, 0xbe, 0x1b, 0x34, 0x4a, 0x24, 0x88, 0xcf, 0x33, 0x21, 0xd6, 0xce, 0x54, 0x2f, + 0x8e, 0x9f, 0x30, 0x55, 0x44, 0xff, 0x09, 0xe4, 0x99, 0x3a, 0x62, 0x31, 0x9a, 0x49, 0x7c, 0x1f, +]; + +/// Compute keccak256 hash +fn keccak256(data: &[u8]) -> [u8; 32] { + let mut hasher = Keccak::v256(); + let mut output = [0u8; 32]; + hasher.update(data); + hasher.finalize(&mut output); + output +} + +/// Compute CREATE2 address: keccak256(0xff ++ deployer ++ salt ++ init_code_hash)[12:] +fn compute_create2_address(deployer: &Address, salt: &B256, init_code_hash: &[u8; 32]) -> Address { + let mut data = [0u8; 85]; + data[0] = 0xff; + data[1..21].copy_from_slice(deployer); + data[21..53].copy_from_slice(salt); + data[53..85].copy_from_slice(init_code_hash); + + let hash = keccak256(&data); + let mut addr = [0u8; 20]; + addr.copy_from_slice(&hash[12..]); + addr +} + +/// Compute CREATE address for nonce=1: keccak256(RLP([address, 1]))[12:] +/// For nonce=1, the RLP encoding is: 0xd6 0x94 <20-byte address> 0x01 +fn compute_create_address_nonce_1(deployer: &Address) -> Address { + let mut data = [0u8; 23]; + data[0] = 0xd6; // 0xc0 + 0x16 (length of: 0x94 + 20 bytes + 0x01) + data[1] = 0x94; // 0x80 + 0x14 (20 bytes) + data[2..22].copy_from_slice(deployer); + data[22] = 0x01; // nonce = 1 + + let hash = keccak256(&data); + let mut addr = [0u8; 20]; + addr.copy_from_slice(&hash[12..]); + addr +} + +/// Compute the final CREATE3 address given a salt and the CreateX deployer address. +/// +/// This matches CreateX's computeCreate3Address function: +/// 1. Compute proxy address via CREATE2 (using the proxy init code hash) +/// 2. Compute final address via CREATE with nonce=1 +pub fn compute_create3_address(salt: &B256, createx_address: &Address) -> Address { + // Step 1: Compute proxy address via CREATE2 + let proxy_address = compute_create2_address(createx_address, salt, &PROXY_INIT_CODE_HASH); + + // Step 2: Compute final address via CREATE (nonce=1) + compute_create_address_nonce_1(&proxy_address) +} + +/// Number of effect steps in the EffectStep enum. +/// Update this constant when adding new steps to the enum. +pub const NUM_EFFECT_STEPS: u32 = 9; + +/// Extract the bitmap from the most significant bits of an address. +/// The bitmap encodes which EffectSteps an effect runs at. +pub fn extract_bitmap(address: &Address) -> u16 { + // Take first 2 bytes and extract top NUM_EFFECT_STEPS bits + // bytes[0] is the MSB, bytes[1] is the next byte + let top_16_bits = ((address[0] as u16) << 8) | (address[1] as u16); + // Shift right to get the top NUM_EFFECT_STEPS bits + top_16_bits >> (16 - NUM_EFFECT_STEPS) +} + +/// Check if an address has the desired bitmap in its most significant bits +pub fn matches_bitmap(address: &Address, target_bitmap: u16) -> bool { + extract_bitmap(address) == target_bitmap +} + +/// Parse an address from a hex string +pub fn parse_address(s: &str) -> Result { + let s = s.trim().trim_start_matches("0x"); + if s.len() != 40 { + return Err(format!("Address must be 40 hex chars, got {}", s.len())); + } + let bytes = hex::decode(s).map_err(|e| format!("Invalid hex: {}", e))?; + let mut addr = [0u8; 20]; + addr.copy_from_slice(&bytes); + Ok(addr) +} + +/// Format an address as a checksummed hex string +pub fn format_address(addr: &Address) -> String { + format!("0x{}", hex::encode(addr)) +} + +/// Format a B256 as a hex string +pub fn format_b256(b: &B256) -> String { + format!("0x{}", hex::encode(b)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_bitmap() { + // For bitmap 0x042 = 0b001000010 (9 bits): + // We need b0 << 1 | (b1 >> 7) = 0x042 + // b0 = 0x042 >> 1 = 0x21 (if LSB of bitmap is 0) + // b1 >> 7 = 0x042 & 1 = 0, so top bit of b1 is 0 + let addr = parse_address("0x2100000000000000000000000000000000000000").unwrap(); + assert_eq!(extract_bitmap(&addr), 0x042); + + // Test bitmap 0x1E0 = 0b111100000 + // b0 = 0x1E0 >> 1 = 0xF0 + // b1 >> 7 = 0, so b1 can be 0x00-0x7F + let addr = parse_address("0xF000000000000000000000000000000000000000").unwrap(); + assert_eq!(extract_bitmap(&addr), 0x1E0); + } + + #[test] + fn test_create3_address_computation() { + // Test against known CreateX deployment + let createx = parse_address("0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed").unwrap(); + + // A zero salt should produce a deterministic address + let salt = [0u8; 32]; + let addr = compute_create3_address(&salt, &createx); + + // The address should be valid (non-zero) + assert_ne!(addr, [0u8; 20]); + + // Verify it's deterministic + let addr2 = compute_create3_address(&salt, &createx); + assert_eq!(addr, addr2); + } +} diff --git a/tools/effect-miner/src/main.rs b/tools/effect-miner/src/main.rs new file mode 100644 index 00000000..f6a6e8ba --- /dev/null +++ b/tools/effect-miner/src/main.rs @@ -0,0 +1,415 @@ +mod create3; +mod miner; + +use crate::create3::{extract_bitmap, format_address, format_b256, parse_address}; +use clap::{Parser, Subcommand}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +/// Effect Address Miner - Mine CREATE3 salts for Effect contracts with specific address bitmaps +#[derive(Parser)] +#[command(name = "effect-miner")] +#[command(about = "Mine vanity addresses for Effect contracts using CREATE3")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Mine a single effect address + Mine { + /// Effect name (for identification) + #[arg(short, long)] + name: String, + + /// Target bitmap (9-bit value, e.g., 0x042 or 66) + #[arg(short, long)] + bitmap: String, + + /// CreateX contract address + #[arg(short, long, default_value = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed")] + createx: String, + + /// Maximum attempts (0 = unlimited) + #[arg(short = 'a', long, default_value = "0")] + max_attempts: u64, + + /// Output file (JSON) + #[arg(short, long)] + output: Option, + }, + + /// Mine multiple effects from a config file + MineAll { + /// Input config file (JSON) + #[arg(short, long)] + config: PathBuf, + + /// CreateX contract address + #[arg(short = 'x', long, default_value = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed")] + createx: String, + + /// Maximum attempts per effect (0 = unlimited) + #[arg(short = 'a', long, default_value = "0")] + max_attempts: u64, + + /// Output file (JSON) + #[arg(short, long)] + output: PathBuf, + }, + + /// Verify an address has the expected bitmap + Verify { + /// Address to verify + #[arg(short, long)] + address: String, + + /// Expected bitmap + #[arg(short, long)] + bitmap: String, + }, + + /// Compute CREATE3 address for a given salt + Compute { + /// Salt (32 bytes hex) + #[arg(short, long)] + salt: String, + + /// CreateX contract address + #[arg(short, long, default_value = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed")] + createx: String, + }, + + /// Generate a config file template with all known effects + GenerateConfig { + /// Output file + #[arg(short, long)] + output: PathBuf, + }, +} + +/// Input config format for mining multiple effects +#[derive(Debug, Serialize, Deserialize)] +struct MiningConfig { + #[serde(default = "default_createx")] + createx: String, + effects: HashMap, +} + +fn default_createx() -> String { + "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed".to_string() +} + +#[derive(Debug, Serialize, Deserialize)] +struct EffectConfig { + bitmap: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, +} + +/// Output format for mined salts +#[derive(Debug, Serialize, Deserialize)] +struct MiningOutput { + createx: String, + effects: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EffectResult { + salt: String, + address: String, + bitmap: String, + attempts: u64, +} + +fn parse_bitmap(s: &str) -> Result { + let s = s.trim().to_lowercase(); + if s.starts_with("0x") { + u16::from_str_radix(&s[2..], 16).map_err(|e| format!("Invalid hex bitmap: {}", e)) + } else if s.starts_with("0b") { + u16::from_str_radix(&s[2..], 2).map_err(|e| format!("Invalid binary bitmap: {}", e)) + } else { + s.parse::().map_err(|e| format!("Invalid decimal bitmap: {}", e)) + } +} + +fn main() { + let cli = Cli::parse(); + + match cli.command { + Commands::Mine { + name, + bitmap, + createx, + max_attempts, + output, + } => { + let bitmap_value = parse_bitmap(&bitmap).expect("Invalid bitmap"); + let createx_addr = parse_address(&createx).expect("Invalid CreateX address"); + + println!("Mining salt for {} with bitmap 0x{:03X}...", name, bitmap_value); + println!("CreateX: {}", createx); + println!("Expected attempts: ~{}", miner::expected_attempts()); + + let result = miner::mine_salt(&createx_addr, bitmap_value, None, max_attempts); + + match result { + Some(r) => { + println!("\nSuccess!"); + println!(" Salt: {}", format_b256(&r.salt)); + println!(" Address: {}", format_address(&r.address)); + println!(" Bitmap: 0x{:03X}", r.bitmap); + println!(" Attempts: {}", r.attempts); + + if let Some(output_path) = output { + let mut effects = HashMap::new(); + effects.insert( + name, + EffectResult { + salt: format_b256(&r.salt), + address: format_address(&r.address), + bitmap: format!("0x{:03X}", r.bitmap), + attempts: r.attempts, + }, + ); + let output = MiningOutput { + createx, + effects, + }; + let json = serde_json::to_string_pretty(&output).unwrap(); + fs::write(&output_path, json).expect("Failed to write output file"); + println!("\nResults written to {:?}", output_path); + } + } + None => { + eprintln!("Failed to find matching salt within {} attempts", max_attempts); + std::process::exit(1); + } + } + } + + Commands::MineAll { + config, + createx, + max_attempts, + output, + } => { + let config_str = fs::read_to_string(&config).expect("Failed to read config file"); + let mining_config: MiningConfig = + serde_json::from_str(&config_str).expect("Failed to parse config file"); + + let createx_addr = parse_address(&createx).expect("Invalid CreateX address"); + + let effects: Vec<(String, u16)> = mining_config + .effects + .iter() + .map(|(name, cfg)| { + let bitmap = parse_bitmap(&cfg.bitmap).expect(&format!( + "Invalid bitmap for {}: {}", + name, cfg.bitmap + )); + (name.clone(), bitmap) + }) + .collect(); + + println!("Mining {} effects...", effects.len()); + println!("CreateX: {}", createx); + println!("Max attempts per effect: {}", if max_attempts == 0 { "unlimited".to_string() } else { max_attempts.to_string() }); + println!(); + + let results = miner::mine_multiple(&createx_addr, effects, max_attempts); + + let mut output_effects = HashMap::new(); + let mut success_count = 0; + let mut fail_count = 0; + + for (name, result) in results { + match result { + Some(r) => { + println!("{}: {} (bitmap: 0x{:03X}, {} attempts)", + name, format_address(&r.address), r.bitmap, r.attempts); + output_effects.insert( + name, + EffectResult { + salt: format_b256(&r.salt), + address: format_address(&r.address), + bitmap: format!("0x{:03X}", r.bitmap), + attempts: r.attempts, + }, + ); + success_count += 1; + } + None => { + eprintln!("{}: FAILED to find matching salt", name); + fail_count += 1; + } + } + } + + println!(); + println!("Complete: {} succeeded, {} failed", success_count, fail_count); + + let mining_output = MiningOutput { + createx, + effects: output_effects, + }; + let json = serde_json::to_string_pretty(&mining_output).unwrap(); + fs::write(&output, json).expect("Failed to write output file"); + println!("Results written to {:?}", output); + } + + Commands::Verify { address, bitmap } => { + let addr = parse_address(&address).expect("Invalid address"); + let expected_bitmap = parse_bitmap(&bitmap).expect("Invalid bitmap"); + let actual_bitmap = extract_bitmap(&addr); + + println!("Address: {}", address); + println!("Expected bitmap: 0x{:03X}", expected_bitmap); + println!("Actual bitmap: 0x{:03X}", actual_bitmap); + + if actual_bitmap == expected_bitmap { + println!("MATCH"); + } else { + println!("MISMATCH"); + std::process::exit(1); + } + } + + Commands::Compute { salt, createx } => { + let salt_hex = salt.trim().trim_start_matches("0x"); + let salt_bytes = hex::decode(salt_hex).expect("Invalid salt hex"); + if salt_bytes.len() != 32 { + eprintln!("Salt must be 32 bytes"); + std::process::exit(1); + } + let mut salt_arr = [0u8; 32]; + salt_arr.copy_from_slice(&salt_bytes); + + let createx_addr = parse_address(&createx).expect("Invalid CreateX address"); + let address = create3::compute_create3_address(&salt_arr, &createx_addr); + let bitmap = extract_bitmap(&address); + + println!("Salt: 0x{}", hex::encode(salt_arr)); + println!("CreateX: {}", createx); + println!("Address: {}", format_address(&address)); + println!("Bitmap: 0x{:03X}", bitmap); + } + + Commands::GenerateConfig { output } => { + let mut effects = HashMap::new(); + + // Core effects + effects.insert("StaminaRegen".to_string(), EffectConfig { + bitmap: "0x042".to_string(), + description: Some("RoundEnd, AfterMove".to_string()), + }); + effects.insert("StatBoosts".to_string(), EffectConfig { + bitmap: "0x008".to_string(), + description: Some("OnMonSwitchOut".to_string()), + }); + effects.insert("Overclock".to_string(), EffectConfig { + bitmap: "0x170".to_string(), + description: Some("OnApply, RoundEnd, OnMonSwitchIn, OnRemove".to_string()), + }); + effects.insert("BurnStatus".to_string(), EffectConfig { + bitmap: "0x1E0".to_string(), + description: Some("OnApply, RoundStart, RoundEnd, OnRemove".to_string()), + }); + effects.insert("FrostbiteStatus".to_string(), EffectConfig { + bitmap: "0x160".to_string(), + description: Some("OnApply, RoundEnd, OnRemove".to_string()), + }); + effects.insert("PanicStatus".to_string(), EffectConfig { + bitmap: "0x1E0".to_string(), + description: Some("OnApply, RoundStart, RoundEnd, OnRemove".to_string()), + }); + effects.insert("SleepStatus".to_string(), EffectConfig { + bitmap: "0x1E0".to_string(), + description: Some("OnApply, RoundStart, RoundEnd, OnRemove".to_string()), + }); + effects.insert("ZapStatus".to_string(), EffectConfig { + bitmap: "0x1E0".to_string(), + description: Some("OnApply, RoundStart, RoundEnd, OnRemove".to_string()), + }); + + // Mon abilities + effects.insert("RiseFromTheGrave".to_string(), EffectConfig { + bitmap: "0x044".to_string(), + description: Some("RoundEnd, AfterDamage".to_string()), + }); + effects.insert("IronWall".to_string(), EffectConfig { + bitmap: "0x00C".to_string(), + description: Some("AfterDamage, OnMonSwitchOut".to_string()), + }); + effects.insert("UpOnly".to_string(), EffectConfig { + bitmap: "0x004".to_string(), + description: Some("AfterDamage".to_string()), + }); + effects.insert("Tinderclaws".to_string(), EffectConfig { + bitmap: "0x042".to_string(), + description: Some("AfterMove, RoundEnd".to_string()), + }); + effects.insert("Q5".to_string(), EffectConfig { + bitmap: "0x080".to_string(), + description: Some("RoundStart".to_string()), + }); + effects.insert("PostWorkout".to_string(), EffectConfig { + bitmap: "0x008".to_string(), + description: Some("OnMonSwitchOut".to_string()), + }); + effects.insert("Baselight".to_string(), EffectConfig { + bitmap: "0x040".to_string(), + description: Some("RoundEnd".to_string()), + }); + effects.insert("CarrotHarvest".to_string(), EffectConfig { + bitmap: "0x040".to_string(), + description: Some("RoundEnd".to_string()), + }); + effects.insert("ActusReus".to_string(), EffectConfig { + bitmap: "0x006".to_string(), + description: Some("AfterMove, AfterDamage".to_string()), + }); + effects.insert("Angery".to_string(), EffectConfig { + bitmap: "0x044".to_string(), + description: Some("RoundEnd, AfterDamage".to_string()), + }); + effects.insert("Dreamcatcher".to_string(), EffectConfig { + bitmap: "0x001".to_string(), + description: Some("OnUpdateMonState".to_string()), + }); + effects.insert("NightTerrors".to_string(), EffectConfig { + bitmap: "0x048".to_string(), + description: Some("RoundEnd, OnMonSwitchOut".to_string()), + }); + effects.insert("Somniphobia".to_string(), EffectConfig { + bitmap: "0x042".to_string(), + description: Some("AfterMove, RoundEnd".to_string()), + }); + effects.insert("Initialize".to_string(), EffectConfig { + bitmap: "0x018".to_string(), + description: Some("OnMonSwitchIn, OnMonSwitchOut".to_string()), + }); + effects.insert("Interweaving".to_string(), EffectConfig { + bitmap: "0x108".to_string(), + description: Some("OnMonSwitchOut, OnApply".to_string()), + }); + effects.insert("ChainExpansion".to_string(), EffectConfig { + bitmap: "0x010".to_string(), + description: Some("OnMonSwitchIn".to_string()), + }); + + let config = MiningConfig { + createx: default_createx(), + effects, + }; + + let json = serde_json::to_string_pretty(&config).unwrap(); + fs::write(&output, json).expect("Failed to write config file"); + println!("Config template written to {:?}", output); + println!("Contains {} effects", config.effects.len()); + } + } +} diff --git a/tools/effect-miner/src/miner.rs b/tools/effect-miner/src/miner.rs new file mode 100644 index 00000000..21f128ec --- /dev/null +++ b/tools/effect-miner/src/miner.rs @@ -0,0 +1,179 @@ +use crate::create3::{compute_create3_address, extract_bitmap, matches_bitmap, Address, B256}; +use rand::Rng; +use rayon::prelude::*; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; + +/// Result of a successful mining operation +#[derive(Debug, Clone)] +pub struct MiningResult { + pub salt: B256, + pub address: Address, + pub bitmap: u16, + pub attempts: u64, +} + +/// Mine a salt that produces an address with the target bitmap in its MSB 9 bits +/// +/// # Arguments +/// * `createx_address` - The CreateX factory contract address +/// * `target_bitmap` - The desired 9-bit bitmap value +/// * `base_salt` - Optional base salt to start from (useful for deterministic mining) +/// * `max_attempts` - Maximum number of attempts before giving up (0 = unlimited) +/// +/// # Returns +/// * `Some(MiningResult)` if a matching salt is found +/// * `None` if max_attempts is reached without finding a match +pub fn mine_salt( + createx_address: &Address, + target_bitmap: u16, + base_salt: Option, + max_attempts: u64, +) -> Option { + let found = Arc::new(AtomicBool::new(false)); + let attempts = Arc::new(AtomicU64::new(0)); + + // Use base_salt or generate random starting points for each thread + let base = base_salt.unwrap_or_else(|| { + let mut rng = rand::thread_rng(); + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes); + bytes + }); + + // Determine chunk size for parallel iteration + let chunk_size = 10_000u64; + let max_chunks = if max_attempts == 0 { + u64::MAX / chunk_size + } else { + (max_attempts + chunk_size - 1) / chunk_size + }; + + let result: Option = (0..max_chunks) + .into_par_iter() + .find_map_any(|chunk_idx| { + if found.load(Ordering::Relaxed) { + return None; + } + + let start = chunk_idx * chunk_size; + let end = if max_attempts == 0 { + start + chunk_size + } else { + std::cmp::min(start + chunk_size, max_attempts) + }; + + for i in start..end { + if found.load(Ordering::Relaxed) { + return None; + } + + // Generate salt by XORing base with counter + let mut salt = base; + let counter_bytes = i.to_be_bytes(); + for (j, &b) in counter_bytes.iter().enumerate() { + salt[24 + j] ^= b; + } + + let address = compute_create3_address(&salt, createx_address); + + attempts.fetch_add(1, Ordering::Relaxed); + + if matches_bitmap(&address, target_bitmap) { + found.store(true, Ordering::Relaxed); + return Some(MiningResult { + salt, + address, + bitmap: extract_bitmap(&address), + attempts: attempts.load(Ordering::Relaxed), + }); + } + } + + None + }); + + result +} + +/// Mine salts for multiple effects in parallel +/// +/// # Arguments +/// * `createx_address` - The CreateX factory contract address +/// * `effects` - List of (effect_name, target_bitmap) tuples +/// * `max_attempts_per_effect` - Maximum attempts per effect (0 = unlimited) +/// +/// # Returns +/// * Vector of (effect_name, Option) tuples +pub fn mine_multiple( + createx_address: &Address, + effects: Vec<(String, u16)>, + max_attempts_per_effect: u64, +) -> Vec<(String, Option)> { + effects + .into_par_iter() + .map(|(name, bitmap)| { + // Use effect name as part of base salt for reproducibility + let mut base_bytes = [0u8; 32]; + let name_bytes = name.as_bytes(); + let copy_len = std::cmp::min(name_bytes.len(), 20); + base_bytes[..copy_len].copy_from_slice(&name_bytes[..copy_len]); + + let result = mine_salt(createx_address, bitmap, Some(base_bytes), max_attempts_per_effect); + (name, result) + }) + .collect() +} + +/// Estimate the expected number of attempts to find a matching address. +/// For an N-bit bitmap, we expect to try ~2^N addresses on average. +pub fn expected_attempts() -> u64 { + 1 << crate::create3::NUM_EFFECT_STEPS // 2^NUM_EFFECT_STEPS +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::create3::parse_address; + + #[test] + fn test_mine_salt() { + let createx = parse_address("0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed").unwrap(); + + // Mine for bitmap 0x042 (StaminaRegen: RoundEnd + AfterMove) + let result = mine_salt(&createx, 0x042, None, 100_000); + + assert!(result.is_some(), "Should find a salt within 100k attempts"); + let result = result.unwrap(); + assert_eq!(result.bitmap, 0x042); + println!( + "Found salt {} -> address {} in {} attempts", + hex::encode(result.salt), + hex::encode(result.address), + result.attempts + ); + } + + #[test] + fn test_mine_multiple() { + let createx = parse_address("0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed").unwrap(); + + let effects = vec![ + ("StaminaRegen".to_string(), 0x042u16), + ("StatBoosts".to_string(), 0x008u16), + ]; + + let results = mine_multiple(&createx, effects, 100_000); + + for (name, result) in results { + assert!(result.is_some(), "Should find salt for {}", name); + let r = result.unwrap(); + println!( + "{}: salt={}, address={}", + name, + hex::encode(r.salt), + hex::encode(r.address) + ); + } + } +}