-
Notifications
You must be signed in to change notification settings - Fork 949
Description
Hellfire Lua Refactoring Proposal
Executive Summary
I asked Claude to generate a proposal to remove the checks gbIsHellfire from the codebase so we can finish moving Hellfire to a Lua Mod. Here is the first draft. I know this is huge, I don't expect anyone to read it fully but it's a really good guideline about what we should prioritize and what is needed to complete this migration.
This document proposes a comprehensive refactoring to eliminate hardcoded gbIsHellfire checks throughout the DevilutionX codebase by replacing them with a Lua event-driven architecture. This will make Hellfire functionality a true mod that can be enabled, disabled, or even replaced without modifying C++ code.
Proposed Architecture
Phase 1: Event-Based Content Registration
New Lua Events
Create events that allow mods to register or modify content:
-- New events to add to devilutionx/events.lua
events.RegisterItems = CreateEvent() -- Register additional items
events.RegisterMonsters = CreateEvent() -- Register additional monsters
events.RegisterSpells = CreateEvent() -- Register additional spells
events.ModifyItemGeneration = CreateEvent() -- Modify item generation rules
events.ModifyMonsterStats = CreateEvent() -- Modify monster statistics
events.ModifyPricing = CreateEvent() -- Modify store pricing
events.ModifyGameMechanics = CreateEvent() -- Modify core game mechanics
events.ValidateContent = CreateEvent() -- Validate content availability
events.SaveGameFormat = CreateEvent() -- Determine save format
events.LoadGameFormat = CreateEvent() -- Determine load formatHellfire Mod Implementation Example
local hellfire = require("devilutionx.hellfire")
local events = require("devilutionx.events")
local items = require("devilutionx.items")
local monsters = require("devilutionx.monsters")
-- Load Hellfire assets
hellfire.loadData()
-- Register Hellfire-specific content
events.RegisterItems.add(function()
-- Register oils and new uniques
items.register({
id = "IDI_OIL_ACCURACY",
-- ... item properties
})
end)
events.RegisterSpells.add(function()
-- Enable Apocalypse and Nova in single player
spells.enable(SpellID.Apocalypse)
spells.enable(SpellID.Nova)
end)
-- Modify game mechanics
events.ModifyMonsterStats.add(function(monster, difficulty)
if difficulty == Difficulty.Nightmare then
if multiplayer then
monster.maxHitPoints = monster.maxHitPoints + (100 << 6)
else
monster.maxHitPoints = monster.maxHitPoints + (50 << 6)
end
elseif difficulty == Difficulty.Hell then
if multiplayer then
monster.maxHitPoints = monster.maxHitPoints + (200 << 6)
else
monster.maxHitPoints = monster.maxHitPoints + (100 << 6)
end
end
end)
-- Modify pricing
events.ModifyPricing.add(function(item, basePrice, vendor)
if vendor == "wirt" then
-- Hellfire: 25% discount
return basePrice - math.floor(basePrice / 4)
end
return basePrice
end)
-- Modify corpse eating mechanics
events.ModifyGameMechanics.add(function(mechanic, context)
if mechanic == "corpse_eating" then
local monster = context.monster
local mMaxHP = monster.maxHitPoints
monster.hitPoints = math.min(
monster.hitPoints + math.floor(mMaxHP / 8),
monster.maxHitPoints
)
-- Stop eating at max HP (Hellfire behavior)
if monster.hitPoints >= monster.maxHitPoints then
monster.goal = MonsterGoal.Normal
end
end
end)Phase 2: Query System for Dynamic Checks
Instead of hardcoded boolean checks, implement a query system:
C++ Side
// New API in lua/lua_api.hpp
namespace devilution {
template<typename T>
std::optional<T> LuaQuery(std::string_view queryName, const sol::object& context = sol::nil);
bool LuaQueryBool(std::string_view queryName, const sol::object& context = sol::nil);
// Example usage
bool isHellfireActive = LuaQueryBool("hellfire.active");
std::optional<int> itemLimit = LuaQuery<int>("hellfire.item_types_count");Lua Side
local queries = require("devilutionx.queries")
-- Register query handlers
queries.register("hellfire.active", function()
return true -- Hellfire mod sets this
end)
queries.register("hellfire.item_types_count", function()
return 52 -- ITEMTYPES for Hellfire
end)
queries.register("content.spell_available", function(spellId)
-- Check if spell is available based on loaded mods
return hellfireLoaded or spellId not in {SpellID.Apocalypse, SpellID.Nova}
end)Phase 3: Strategy Pattern for Behavior Variations
For complex behavioral differences, use a strategy/policy pattern:
// New system in Source/strategies/
class GameMechanicsStrategy {
public:
virtual ~GameMechanicsStrategy() = default;
virtual int CalculateMonsterHP(Monster& monster, Difficulty diff) = 0;
virtual int CalculateWirtPrice(const Item& item, int basePrice) = 0;
virtual bool ShouldStopEating(const Monster& monster) = 0;
};
// Default (Diablo) strategy
class DiabloStrategy : public GameMechanicsStrategy {
// Original Diablo behavior
};
// Hellfire strategy (loaded from Lua)
class LuaStrategy : public GameMechanicsStrategy {
// Calls Lua event handlers
};
// Global instance
extern std::unique_ptr<GameMechanicsStrategy> g_mechanicsStrategy;Phase 4: Save/Load Format Registry
Handle save format differences through a versioning system:
// In loadsave.cpp
enum class SaveFormat {
Diablo,
Hellfire,
Custom
};
struct SaveFormatHandler {
std::string name;
std::function<void(SaveHelper&, Player&)> save;
std::function<void(LoadHelper&, Player&)> load;
};
std::unordered_map<SaveFormat, SaveFormatHandler> g_saveFormats;
// Register from Lua
void RegisterSaveFormat(std::string_view name, sol::function saveFn, sol::function loadFn) {
// ...
}Implementation Roadmap
Milestone 1: Infrastructure (Weeks 1-2)
- Implement new Lua event types
- Create query system
- Add Lua bindings for necessary C++ types
- Create migration utilities
Milestone 2: Content Registration (Weeks 3-4)
- Refactor item registration (~41 occurrences)
- Refactor spell availability
- Refactor monster registration
- Move Hellfire-specific content to Lua
Milestone 3: Game Mechanics (Weeks 5-6)
- Implement strategy pattern
- Refactor monster mechanics (~10 occurrences)
- Refactor item generation
- Refactor pricing and stores
Milestone 4: Save/Load (Weeks 7-8)
- Implement save format registry (~35 occurrences)
- Create compatibility layer
- Test save/load across versions
- Document format changes
Milestone 5: Testing & Polish (Weeks 9-10)
- Comprehensive testing with/without Hellfire
- Performance optimization
- Documentation
- Migration guide for other mods
Challenges & Risks
Performance
Risk: Lua calls add overhead
Mitigation:
- Cache query results where possible
- Use bulk operations for content registration
- Profile and optimize hot paths
- Consider JIT compilation for critical paths
Compatibility
Risk: Breaking existing saves/mods
Mitigation:
- Maintain backward compatibility layer
- Version save files appropriately
- Provide migration tools
- Thorough testing
Complexity
Risk: Initial implementation is complex
Mitigation:
- Incremental migration (phase by phase)
- Maintain parallel implementations during transition
- Comprehensive documentation
- Code reviews at each milestone
API Design
Risk: Getting the Lua API wrong
Mitigation:
- Prototype with a few use cases first
- Get community feedback early
- Keep APIs flexible but not too broad
- Document design decisions
Appendix A: Affected Files
High Priority (10+ occurrences)
Source/items.cpp(41)Source/loadsave.cpp(35)Source/monster.cpp(10)
Medium Priority (5-9 occurrences)
Source/pfile.cpp(9)Source/missiles.cpp(7)Source/stores.cpp(4)
Low Priority (1-4 occurrences)
- All other 36 files with 1-4 occurrences each
Appendix B: Lua API Extensions Needed
New Modules
devilutionx.queries- Query systemdevilutionx.content- Content registrationdevilutionx.saveformat- Save format handling
Extended Modules
devilutionx.items- Add registration methodsdevilutionx.monsters- Add registration methodsdevilutionx.spells- Add enable/disable methodsdevilutionx.hellfire- Expand beyond just enable()
New Events
RegisterItemsRegisterMonstersRegisterSpellsModifyItemGenerationModifyMonsterStatsModifyPricingModifyGameMechanicsValidateContentSaveGameFormatLoadGameFormat
Appendix C: Performance Considerations
Hot Paths to Watch
- Item generation (called frequently during gameplay)
- Monster stat calculations (called at spawn)
- Combat calculations (if any Hellfire-specific logic)
Optimization Strategies
- Caching: Cache query results for static queries
- Batch Operations: Register content in batches at startup
- Native Paths: Keep hot paths in C++ with Lua override option
- Lazy Evaluation: Defer Lua calls until actually needed
Benchmarking Targets
- No more than 5% performance regression in item generation
- No more than 1% regression in combat frame rate
- Save/load times should remain within 10% of current
Appendix D: Migration Checklist
For each gbIsHellfire check:
- Identify category (content, mechanics, save/load, UI, validation)
- Determine appropriate event or query
- Create Lua API if needed
- Implement C++ side
- Implement Hellfire mod side
- Implement Diablo default side (if needed)
- Add tests
- Update documentation
- Remove original
gbIsHellfirecheck