Skip to content

Lua custom items#8550

Open
yuripourre wants to merge 17 commits into
diasurgical:masterfrom
yuripourre:lua-custom-items
Open

Lua custom items#8550
yuripourre wants to merge 17 commits into
diasurgical:masterfrom
yuripourre:lua-custom-items

Conversation

@yuripourre
Copy link
Copy Markdown
Collaborator

@yuripourre yuripourre commented Apr 22, 2026

PR to enable custom items using Lua

  • Add method to register/free custom item graphics
  • Add helper methods to get item graphics (instead of directly from the array)
  • Add method to create items using lua

Loading regular diablo save files do not break.

Example of Mod - Katar (as a sword)

local events = require("devilutionx.events")
local items = require("devilutionx.items")
local audio = require("devilutionx.audio")

events.ItemDataLoaded.add(function()
  local cursId = items.registerCursorGraphic("lua\\mods\\katar\\objcur\\katar", 28)
  -- Register the item
  items.addItem({
    name = "Katar",
    shortName = "Katar",
    mappingId = 50000,
    type = items.ItemType.Sword,
    class = items.ItemClass.Weapon,equipType = items.ItemEquipType.OneHand,
    cursorGraphic = cursId,
    minDam = 4,
    maxDam = 7,
    durability = 30,
    value = 100
  })
end)
katar.mp4

File with the mod katar.mpq
katar_mod.zip

@HoofedEar
Copy link
Copy Markdown
Contributor

Love this!
Looks like this is specifically adding a new equipment in your example right? Would there be support for adding a consumable (with a custom effect) or will that be something separate?

@yuripourre
Copy link
Copy Markdown
Collaborator Author

Love this! Looks like this is specifically adding a new equipment in your example right? Would there be support for adding a consumable (with a custom effect) or will that be something separate?

It can be done, I only created a weapon because it would be a more complex example but I can try to create a consumable. @HoofedEar do you have anything specific in mind?

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables Lua-defined custom items by allowing mods to register custom cursor/drop graphics and sounds, and by routing item animation/sound lookups through helper functions that safely handle custom cursor IDs. It also adjusts save-loading to better preserve modded items.

Changes:

  • Add custom cursor/drop graphic + sound registration APIs and safe lookup helpers (GetItemAnimType, GetItemInvSnd, GetItemDropSnd).
  • Expose Lua APIs to define items/uniques and register associated graphics/sounds.
  • Update inventory/stash/store UI call sites to use the new helpers; add unit tests and wire them into CMake.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
test/custom_items_test.cpp Adds unit tests for new item anim/sound lookup and custom item registration behavior.
Source/tables/itemdat.cpp Clears custom cursor sprites before reloading item data.
Source/qol/visual_store.cpp Uses GetItemInvSnd instead of direct table lookup.
Source/qol/stash.cpp Uses GetItemInvSnd instead of direct table lookup.
Source/qol/itemlabels.cpp Uses GetItemAnimType instead of direct table lookup.
Source/lua/modules/items.cpp Adds Lua APIs for adding items/uniques and registering graphics/sounds.
Source/loadsave.cpp Preserves unpacked items when heroitems data can’t be loaded/recreated.
Source/items.h Declares new custom item APIs and exposes ItemCAnimTblSize.
Source/items.cpp Implements custom drop anim/sound registries + safe lookup helpers.
Source/inv.cpp Uses GetItemInvSnd instead of direct table lookup.
Source/cursor.h Declares custom cursor sprite registration/free APIs.
Source/cursor.cpp Implements custom cursor sprite storage and lookup in GetInvItemSprite.
CMake/Tests.cmake Adds custom_items_test to the test suite.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Source/tables/itemdat.cpp
Comment thread Source/cursor.cpp
Comment thread Source/items.cpp Outdated
Comment thread Source/lua/modules/items.cpp Outdated
Comment thread Source/lua/modules/items.cpp Outdated
Comment thread test/custom_items_test.cpp
Comment thread test/custom_items_test.cpp
Comment thread Source/cursor.cpp
Comment thread Source/items.cpp Outdated
@HoofedEar
Copy link
Copy Markdown
Contributor

It can be done, I only created a weapon because it would be a more complex example but I can try to create a consumable. @HoofedEar do you have anything specific in mind?

Coming from that source mod I was working on, where I added an item that turns a normal equipment into a magical one, I remember having to define what the cursor sprite would be when consumed, and then the effect that applies it. But I believe I went the route of creating a spell, so consuming the item was similar to reading a scroll. But I'm not sure what the best way to do that in Lua would be hmm. I guess "recreating" a scroll with a different name might be a good starting point?

@yuripourre
Copy link
Copy Markdown
Collaborator Author

It can be done, I only created a weapon because it would be a more complex example but I can try to create a consumable. @HoofedEar do you have anything specific in mind?

Coming from that source mod I was working on, where I added an item that turns a normal equipment into a magical one, I remember having to define what the cursor sprite would be when consumed, and then the effect that applies it. But I believe I went the route of creating a spell, so consuming the item was similar to reading a scroll. But I'm not sure what the best way to do that in Lua would be hmm. I guess "recreating" a scroll with a different name might be a good starting point?

Hmm, if you have that spell already created, I think the new item can be used as a scroll. I can try that! But the new spell cannot be created using only lua, right?

@HoofedEar
Copy link
Copy Markdown
Contributor

Coming from that source mod I was working on, where I added an item that turns a normal equipment into a magical one, I remember having to define what the cursor sprite would be when consumed, and then the effect that applies it. But I believe I went the route of creating a spell, so consuming the item was similar to reading a scroll. But I'm not sure what the best way to do that in Lua would be hmm. I guess "recreating" a scroll with a different name might be a good starting point?

Hmm, if you have that spell already created, I think the new item can be used as a scroll. I can try that! But the new spell cannot be created using only lua, right?

I believe yes, for certain effects we'd probably have to write custom code
So in my mind, the mod would be two lua files: one for the spell, and then one for the item that is a consumable that uses the spell (kinda like scroll of identify)

Definitely not trying to scope creep this haha just thinking out loud

@StephenCWills
Copy link
Copy Markdown
Member

You need to add DVL_API_FOR_TEST to the extern declarations in the header files for the following global variables:

  • ItemMappingIdsToIndices
  • ItemCAnimTbl
  • ItemCAnimTblSize

@yuripourre
Copy link
Copy Markdown
Collaborator Author

You need to add DVL_API_FOR_TEST to the extern declarations in the header files for the following global variables:

* `ItemMappingIdsToIndices`

* `ItemCAnimTbl`

* `ItemCAnimTblSize`

Done!

@yuripourre yuripourre force-pushed the lua-custom-items branch 2 times, most recently from 3a7ff02 to 8269f25 Compare April 27, 2026 19:45
@yuripourre
Copy link
Copy Markdown
Collaborator Author

This PR is a superset (and lua version) of: #8410

@yuripourre yuripourre added the pr size:large Pull Requests that add more than 100 lines label May 2, 2026
@StephenCWills
Copy link
Copy Markdown
Member

  -- Register the item
  items.addItem({
    name = "Katar",
    shortName = "Katar",
    mappingId = 50000,
    type = items.ItemType.Sword,
    class = items.ItemClass.Weapon,equipType = items.ItemEquipType.OneHand,
    cursorGraphic = cursId,
    minDam = 4,
    maxDam = 7,
    durability = 30,
    value = 100
  })

There are a couple things that bother me about this API compared to the TSV functions.

  • To remain consistent with the existing function name--addItemDataFromTsv()--it should probably be called addItemData().
  • The signature for addItemDataFromTsv(path: string, baseMappingId: number) is significantly more user-friendly.
    • path points to a TSV file which, theoretically, encapsulates all of your mod's item data. It represents a collection of items.
    • baseMappingId is a single number your mod assigns to that collection of item data. Two mods which pick largely different numbers will have no conflicts. If mods do have conflicts, the user only has to change this one number to work around it.

The addItemData() function might work better for me if it takes an array of item data tables and a single base mapping ID. That being said, I'm not crazy about how the current TSV-based functions will crash the application due to conflicts between mods in the first place. I think it might be worth changing save files and the API to make the mapping ID less rigid.

@yuripourre
Copy link
Copy Markdown
Collaborator Author

  -- Register the item
  items.addItem({
    name = "Katar",
    shortName = "Katar",
    mappingId = 50000,
    type = items.ItemType.Sword,
    class = items.ItemClass.Weapon,equipType = items.ItemEquipType.OneHand,
    cursorGraphic = cursId,
    minDam = 4,
    maxDam = 7,
    durability = 30,
    value = 100
  })

There are a couple things that bother me about this API compared to the TSV functions.

* To remain consistent with the existing function name--`addItemDataFromTsv()`--it should probably be called `addItemData()`.

* The signature for `addItemDataFromTsv(path: string, baseMappingId: number)` is significantly more user-friendly.
  
  * `path` points to a TSV file which, theoretically, encapsulates all of your mod's item data. It represents a _collection_ of items.
  * `baseMappingId` is a single number your mod assigns to that collection of item data. Two mods which pick largely different numbers will have no conflicts. If mods do have conflicts, the user only has to change this one number to work around it.

The addItemData() function might work better for me if it takes an array of item data tables and a single base mapping ID. That being said, I'm not crazy about how the current TSV-based functions will crash the application due to conflicts between mods in the first place. I think it might be worth changing save files and the API to make the mapping ID less rigid.

Thank you so so much for reviewing!! I did the changes, hope I could capture everything you meant.

@StephenCWills
Copy link
Copy Markdown
Member

Looks like you need to add DVL_API_FOR_TEST to UniqueItemMappingIdsToIndices this time.

@yuripourre
Copy link
Copy Markdown
Collaborator Author

Looks like you need to add DVL_API_FOR_TEST to UniqueItemMappingIdsToIndices this time.

Thank you @StephenCWills! Just added.

@yuripourre yuripourre self-assigned this May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr size:large Pull Requests that add more than 100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants