Skip to content

[Feature Request]: Hellfire as a Lua Mod #8451

@yuripourre

Description

@yuripourre

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 format

Hellfire 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)

  1. Implement new Lua event types
  2. Create query system
  3. Add Lua bindings for necessary C++ types
  4. Create migration utilities

Milestone 2: Content Registration (Weeks 3-4)

  1. Refactor item registration (~41 occurrences)
  2. Refactor spell availability
  3. Refactor monster registration
  4. Move Hellfire-specific content to Lua

Milestone 3: Game Mechanics (Weeks 5-6)

  1. Implement strategy pattern
  2. Refactor monster mechanics (~10 occurrences)
  3. Refactor item generation
  4. Refactor pricing and stores

Milestone 4: Save/Load (Weeks 7-8)

  1. Implement save format registry (~35 occurrences)
  2. Create compatibility layer
  3. Test save/load across versions
  4. Document format changes

Milestone 5: Testing & Polish (Weeks 9-10)

  1. Comprehensive testing with/without Hellfire
  2. Performance optimization
  3. Documentation
  4. 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 system
  • devilutionx.content - Content registration
  • devilutionx.saveformat - Save format handling

Extended Modules

  • devilutionx.items - Add registration methods
  • devilutionx.monsters - Add registration methods
  • devilutionx.spells - Add enable/disable methods
  • devilutionx.hellfire - Expand beyond just enable()

New Events

  • RegisterItems
  • RegisterMonsters
  • RegisterSpells
  • ModifyItemGeneration
  • ModifyMonsterStats
  • ModifyPricing
  • ModifyGameMechanics
  • ValidateContent
  • SaveGameFormat
  • LoadGameFormat

Appendix C: Performance Considerations

Hot Paths to Watch

  1. Item generation (called frequently during gameplay)
  2. Monster stat calculations (called at spawn)
  3. Combat calculations (if any Hellfire-specific logic)

Optimization Strategies

  1. Caching: Cache query results for static queries
  2. Batch Operations: Register content in batches at startup
  3. Native Paths: Keep hot paths in C++ with Lua override option
  4. 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 gbIsHellfire check

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions