Added custom-raw-tokens utility (#2038)

* Added (chain) for [CHAIN_METAL_TEXT] armours in gui/materials.lua used by gui/create-item-- again (oops)

* Added customRawData utility

* Oops, whitespace

* Revised rawStringsFieldNames

* Dialed down on lua trickery and fixed wrongly formatted changelog entry

* Fixed changelog in wrong place and made customRawData a proper module

* Fixed not caching not-present tags, revised examples and fixed error

* Fixed whitespace. Changing settings in editor!

* customRawData docs

* Added getCreatureTag for respecting caste tags, "fixed" bizarre caching error (quotes because I don't even know what was causing it) and updated docs

* Added line limiting for docs, I guess

* Added missing string convert argument

* docs indent fix, code block fix, and revision

* Major revision

* gdi, docs error

* Another? But... huh.

* ...

* Made requested changes

* Whoops

* Rearrange docs lines

* Followed example, should fix linter issues

* fix typo. linted offline this time......

* Make it so that last instance of tag is what is read from

* Added requested change

* eventful key change

* i to lenArgs

* change eventful key

* add test for broken caste selection

* Major redesign

* tags --> tokens

* Added plant growth behaviour and did some requested changes

* More error handling

* fix docs

* Added basic error suppression

* Docs clarification.

* Docs registering example and fix error

* Strip errors on frame after onWorldLoad, not on map load

* Revert "Strip errors on frame after onWorldLoad, not on map load"

This reverts commit e20a0ef8d3743f79d961077f46910b77b16f36b9.

* Revert "Docs registering example and fix error"

This reverts commit 9c848c54c3f84e0ecc1dc421137c8a8b4a52280d.

* Revert "Docs clarification."

This reverts commit 6b4b6a1aa40c50398504f37ecf1ff0f93d6459b1.

* Revert "Added basic error suppression"

This reverts commit d11cb1438cf1e56ff700469e944f0b9af64651d7.

* Use more eventful key more consistent with other files

* use onStateChange instead of eventful and remove redundant utils require

* Code review stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update docs/Lua API.rst

committing a suggestion

Co-authored-by: Alan <lethosor@users.noreply.github.com>

* Prepend examples with DFHACK_

* Remove unused parameters

* Use new ensure_key global

* Named a couple of unnamed arguments (untested)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Alan <lethosor@users.noreply.github.com>
develop
Tachytaenius 2022-04-29 16:39:59 +01:00 committed by GitHub
parent f08a268e8a
commit b9c36c1e63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 436 additions and 0 deletions

@ -3198,6 +3198,80 @@ Predefined instance methods:
To avoid confusion, these methods cannot be redefined. To avoid confusion, these methods cannot be redefined.
.. _custom-raw-tokens:
custom-raw-tokens
=================
A module for reading custom tokens added to the raws by mods.
* ``customRawTokens.getToken(typeDefinition, token)``
Where ``typeDefinition`` is a type definition struct as seen in ``df.global.world.raws``
(e.g.: ``dfhack.gui.getSelectedItem().subtype``) and ``token`` is the name of the custom token
you want read. The arguments from the token will then be returned as strings using single or
multiple return values. If the token is not present, the result is false; if it is present
but has no arguments, the result is true. For ``creature_raw``, it checks against no caste.
For ``plant_raw``, it checks against no growth.
* ``customRawTokens.getToken(typeInstance, token)``
Where ``typeInstance`` is a unit, entity, item, job, projectile, building, plant, or interaction
instance. Gets ``typeDefinition`` and then returns the same as ``getToken(typeDefinition, token)``.
For units, it gets the token from the race or caste instead if appplicable. For plants growth items,
it gets the token from the plant or plant growth instead if applicable. For plants it does the same
but with growth number -1.
* ``customRawTokens.getToken(raceDefinition, casteNumber, token)``
The same as ``getToken(unit, token)`` but with a specified race and caste. Caste number -1 is no caste.
* ``customRawTokens.getToken(raceDefinition, casteName, token)``
The same as ``getToken(unit, token)`` but with a specified race and caste, using caste name (e.g. "FEMALE")
instead of number.
* ``customRawTokens.getToken(plantDefinition, growthNumber, token)``
The same as ``getToken(plantGrowthItem, token)`` but with a specified plant and growth. Growth number -1
is no growth.
* ``customRawTokens.getToken(plantDefinition, growthName, token)``
The same as ``getToken(plantGrowthItem, token)`` but with a specified plant and growth, using growth name
(e.g. "LEAVES") instead of number.
Examples:
* Using an eventful onReactionComplete hook, something for disturbing dwarven science::
if customRawTokens.getToken(reaction, "DFHACK_CAUSES_INSANITY") then
-- make unit who performed reaction go insane
* Using an eventful onProjItemCheckMovement hook, a fast or slow-firing crossbow::
-- check projectile distance flown is zero, get firer, etc...
local multiplier = tonumber(customRawTokens.getToken(bow, "DFHACK_FIRE_RATE_MULTIPLIER")) or 1
firer.counters.think_counter = firer.counters.think_counter * multiplier
* Something for a script that prints help text about different types of units::
local unit = dfhack.gui.getSelectedUnit()
if not unit then return end
local helpText = customRawTokens.getToken(unit, "DFHACK_HELP_TEXT")
if helpText then print(helpText) end
* Healing armour::
-- (per unit every tick)
local healAmount = 0
for _, entry in ipairs(unit.inventory) do
if entry.mode == 2 then -- Worn
healAmount = healAmount + tonumber((customRawTokens.getToken(entry.item, "DFHACK_HEAL_AMOUNT")) or 0)
end
end
unit.body.blood_count = math.min(unit.body.blood_max, unit.body.blood_count + healAmount)
================== ==================
In-game UI Library In-game UI Library
================== ==================

@ -78,6 +78,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- ``widgets.FilteredList`` now allows all punctuation to be typed into the filter and can match search keys that start with punctuation. - ``widgets.FilteredList`` now allows all punctuation to be typed into the filter and can match search keys that start with punctuation.
- ``widgets.ListBox``: minimum height of dialog is now calculated correctly when there are no items in the list (e.g. when a filter doesn't match anything) - ``widgets.ListBox``: minimum height of dialog is now calculated correctly when there are no items in the list (e.g. when a filter doesn't match anything)
- Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` - Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)``
- Added `custom-raw-tokens` utility to Lua library for reading tokens added to raws by mods.
- ``dwarfmode.MenuOverlay``: if ``sidebar_mode`` attribute is set, automatically manage entering a specific sidebar mode on show and restoring the previous sidebar mode on dismiss - ``dwarfmode.MenuOverlay``: if ``sidebar_mode`` attribute is set, automatically manage entering a specific sidebar mode on show and restoring the previous sidebar mode on dismiss
- ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface where the default designation mode is ``DesignateChopTrees`` - ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface where the default designation mode is ``DesignateChopTrees``
- New string class function: ``string:escape_pattern()`` escapes regex special characters within a string - New string class function: ``string:escape_pattern()`` escapes regex special characters within a string

@ -0,0 +1,361 @@
--[[
custom-raw-tokens
Allows for reading custom tokens added to raws by mods
by Tachytaenius (wolfboyft)
Yes, non-vanilla raw tokens do quietly print errors into the error log but the error log gets filled with garbage anyway
NOTE: This treats plant growths similarly to creature castes but there is no way to deselect a growth, so don't put a token you want to apply to a whole plant after any growth definitions
]]
local _ENV = mkmodule("custom-raw-tokens")
local customRawTokensCache = {}
dfhack.onStateChange.customRawTokens = function(code)
if code == SC_WORLD_UNLOADED then
customRawTokensCache = {}
end
end
local function doToken(cacheTable, token, iter)
local args, lenArgs = {}, 0
for arg in iter do
lenArgs = lenArgs + 1
args[lenArgs] = arg
end
if lenArgs == 0 then
cacheTable[token] = true
return true
else
cacheTable[token] = args
return table.unpack(args)
end
end
local function getSubtype(item)
if item:getSubtype() == -1 then return nil end -- number
return dfhack.items.getSubtypeDef(item:getType(), item:getSubtype()) -- struct
end
local rawStringsFieldNames = {
[df.inorganic_raw] = "str",
[df.plant_raw] = "raws",
[df.creature_raw] = "raws",
[df.itemdef_weaponst] = "raw_strings",
[df.itemdef_trapcompst] = "raw_strings",
[df.itemdef_toyst] = "raw_strings",
[df.itemdef_toolst] = "raw_strings",
[df.itemdef_instrumentst] = "raw_strings",
[df.itemdef_armorst] = "raw_strings",
[df.itemdef_ammost] = "raw_strings",
[df.itemdef_siegeammost] = "raw_strings",
[df.itemdef_glovesst] = "raw_strings",
[df.itemdef_shoesst] = "raw_strings",
[df.itemdef_shieldst] = "raw_strings",
[df.itemdef_helmst] = "raw_strings",
[df.itemdef_pantsst] = "raw_strings",
[df.itemdef_foodst] = "raw_strings",
[df.entity_raw] = "raws",
[df.language_word] = "str",
[df.language_symbol] = "str",
[df.language_translation] = "str",
[df.reaction] = "raw_strings",
[df.interaction] = "str"
}
local function getTokenCore(typeDefinition, token)
-- Have we got a table for this item subtype/reaction/whatever?
-- tostring is needed here because the same raceDefinition key won't give the same value every time
local thisTypeDefCache = ensure_key(customRawTokensCache, tostring(typeDefinition))
-- Have we already extracted and stored this custom raw token for this type definition?
local tokenData = thisTypeDefCache[token]
if tokenData ~= nil then
if type(tokenData) == "table" then
return table.unpack(tokenData)
else
return tokenData
end
end
-- Get data anew
local success, dftype = pcall(function() return typeDefinition._type end)
local rawStrings = typeDefinition[rawStringsFieldNames[dftype]]
if not success or not rawStrings then
error("Expected a raw type definition or instance in argument 1")
end
local currentTokenIterator
for _, rawString in ipairs(rawStrings) do -- e.g. "[CUSTOM_TOKEN:FOO:2]"
local noBrackets = rawString.value:sub(2, -2)
local iter = noBrackets:gmatch("[^:]*") -- iterate over all the text between colons between the brackets
if token == iter() then
currentTokenIterator = iter -- we return for last instance of token if multiple instances are present
end
end
if currentTokenIterator then
return doToken(thisTypeDefCache, token, currentTokenIterator)
end
-- Not present
thisTypeDefCache[token] = false
return false
end
local function getRaceCasteTokenCore(raceDefinition, casteNumber, token)
-- Have we got tables for this race/caste pair?
local thisRaceDefCache = ensure_key(customRawTokensCache, tostring(raceDefinition))
local thisRaceDefCacheCaste = ensure_key(thisRaceDefCache, casteNumber)
-- Have we already extracted and stored this custom raw token for this race/caste pair?
local tokenData = thisRaceDefCacheCaste[token]
if tokenData ~= nil then
if type(tokenData) == "table" then
return table.unpack(tokenData)
elseif tokenData == false and casteNumber ~= -1 then
return getRaceCasteTokenCore(raceDefinition, -1, token)
else
return tokenData
end
end
-- Get data anew. Here we have to track what caste is currently being written to
local casteId, thisCasteActive
if casteNumber ~= -1 then
casteId = raceDefinition.caste[casteNumber].caste_id
thisCasteActive = false
else
thisCasteActive = true
end
local currentTokenIterator
for _, rawString in ipairs(raceDefinition.raws) do
local noBrackets = rawString.value:sub(2, -2)
local iter = noBrackets:gmatch("[^:]*")
local rawStringToken = iter()
if rawStringToken == "CASTE" or rawStringToken == "SELECT_CASTE" or rawStringToken == "SELECT_ADDITIONAL_CASTE" or rawStringToken == "USE_CASTE" then
local newCaste = iter()
if newCaste then
thisCasteActive = newCaste == casteId or rawStringToken == "SELECT_CASTE" and newCaste == "ALL"
end
elseif thisCasteActive and token == rawStringToken then
currentTokenIterator = iter
end
end
if currentTokenIterator then
return doToken(thisRaceDefCache, token, currentTokenIterator)
end
thisRaceDefCacheCaste[token] = false
if casteNumber == -1 then
return false -- Don't get into an infinite loop!
end
-- Not present, try with no caste
return getRaceCasteTokenCore(raceDefinition, -1, token)
end
local function getPlantGrowthTokenCore(plantDefinition, growthNumber, token)
-- Have we got tables for this plant/growth pair?
local thisPlantDefCache = ensure_key(customRawTokensCache, tostring(plantDefinition))
local thisPlantDefCacheGrowth = ensure_key(thisPlantDefCache, growthNumber)
-- Have we already extracted and stored this custom raw token for this plant/growth pair?
local tokenData = thisPlantDefCacheGrowth[token]
if tokenData ~= nil then
if type(tokenData) == "table" then
return table.unpack(tokenData)
elseif tokenData == false and growthNumber ~= -1 then
return getPlantGrowthTokenCore(plantDefinition, -1, token)
else
return tokenData
end
end
-- Get data anew. Here we have to track what growth is currently being written to
local growthId, thisGrowthActive
if growthNumber ~= -1 then
growthId = plantDefinition.growths[growthNumber].id
thisGrowthActive = false
else
thisGrowthActive = true
end
local currentTokenIterator
for _, rawString in ipairs(plantDefinition.raws) do
local noBrackets = rawString.value:sub(2, -2)
local iter = noBrackets:gmatch("[^:]*")
local rawStringToken = iter()
if rawStringToken == "GROWTH" then
local newGrowth = iter()
if newGrowth then
thisGrowthActive = newGrowth == growthId
end
elseif thisGrowthActive and token == rawStringToken then
currentTokenIterator = iter
end
end
if currentTokenIterator then
return doToken(thisPlantDefCache, token, currentTokenIterator)
end
thisPlantDefCacheGrowth[token] = false
if growthNumber == -1 then
return false
end
return getPlantGrowthTokenCore(plantDefinition, -1, token)
end
--[[
Function signatures:
getToken(rawStruct, token)
getToken(rawStructInstance, token)
getToken(raceDefinition, casteNumber, token)
getToken(raceDefinition, casteString, token)
getToken(plantDefinition, growthNumber, token)
getToken(plantDefinition, growthString, token)
]]
local function getTokenArg1RaceDefinition(raceDefinition, b, c)
local casteNumber, token
if not c then
-- 2 arguments
casteNumber = -1
assert(type(b) == "string", "Invalid argument 2 to getToken, must be a string")
token = b
elseif type(b) == "number" then
-- 3 arguments, casteNumber
assert(b == -1 or b < #raceDefinition.caste and math.floor(b) == b and b >= 0, "Invalid argument 2 to getToken, must be -1 or a caste name or number present in the creature raw")
casteNumber = b
assert(type(c) == "string", "Invalid argument 3 to getToken, must be a string")
token = c
else
-- 3 arguments, casteString
assert(type(b) == "string", "Invalid argument 2 to getToken, must be -1 or a caste name or number present in the creature raw")
local casteString = b
for i, v in ipairs(raceDefinition.caste) do
if v.caste_id == casteString then
casteNumber = i
break
end
end
assert(casteNumber, "Invalid argument 2 to getToken, caste name \"" .. casteString .. "\" not found")
assert(type(c) == "string", "Invalid argument 3 to getToken, must be a string")
token = c
end
return getRaceCasteTokenCore(raceDefinition, casteNumber, token)
end
local function getTokenArg1PlantDefinition(plantDefinition, b, c)
local growthNumber, token
if not c then
-- 2 arguments
growthNumber = -1
assert(type(b) == "string", "Invalid argument 2 to getToken, must be a string")
token = b
elseif type(b) == "number" then
-- 3 arguments, growthNumber
assert(b == -1 or b < #plantDefinition.growths and math.floor(b) == b and b >= 0, "Invalid argument 2 to getToken, must be -1 or a growth name or number present in the plant raw")
growthNumber = b
assert(type(c) == "string", "Invalid argument 3 to getToken, must be a string")
token = c
else
-- 3 arguments, growthString
assert(type(b) == "string", "Invalid argument 2 to getToken, must be -1 or a growth name or number present in the plant raw")
local growthString = b
for i, v in ipairs(plantDefinition.growths) do
if v.id == growthString then
growthNumber = i
break
end
end
assert(growthNumber, "Invalid argument 2 to getToken, growth name \"" .. growthString .. "\" not found")
assert(type(c) == "string", "Invalid argument 3 to getToken, must be a string")
token = c
end
return getPlantGrowthTokenCore(plantDefinition, growthNumber, token)
end
local function getTokenArg1Else(userdata, token)
assert(type(token) == "string", "Invalid argument 2 to getToken, must be a string")
local rawStruct
if df.is_instance(df.historical_entity, userdata) then
rawStruct = userdata.entity_raw
elseif df.is_instance(df.item, userdata) then
rawStruct = getSubtype(userdata)
elseif df.is_instance(df.job, userdata) then
if job.job_type == df.job_type.CustomReaction then
for i, v in ipairs(df.global.world.raws.reactions.reactions) do
if job.reaction_name == v.code then
rawStruct = v
break
end
end
end
elseif df.is_instance(df.proj_itemst, userdata) then
if not userdata.item then return false end
if df.is_instance(df.item_plantst, userdata.item) or df.is_instance(df.item_plant_growthst, userdata.item) then
-- use plant behaviour from getToken
return getToken(userdata.item, token)
end
rawStruct = userdata.item and userdata.item.subtype
elseif df.is_instance(df.proj_unitst, userdata) then
if not usertdata.unit then return false end
-- special return so do tag here
local unit = userdata.unit
return getRaceCasteTokenCore(df.global.world.raws.creatures.all[unit.race], unit.caste, token)
elseif df.is_instance(df.building_workshopst, userdata) or df.is_instance(df.building_furnacest, userdata) then
rawStruct = df.building_def.find(userdata.custom_type)
elseif df.is_instance(df.interaction_instance, userdata) then
rawStruct = df.global.world.raws.interactions[userdata.interaction_id]
else
-- Assume raw struct *is* argument 1
rawStruct = userdata
end
if not rawStruct then return false end
return getTokenCore(rawStruct, token)
end
function getToken(from, b, c)
-- Argument processing
assert(from and type(from) == "userdata", "Expected userdata for argument 1 to getToken")
if df.is_instance(df.creature_raw, from) then
-- Signatures from here:
-- getToken(raceDefinition, casteNumber, token)
-- getToken(raceDefinition, casteString, token)
return getTokenArg1RaceDefinition(from, b, c)
elseif df.is_instance(df.unit, from) then
-- Signatures from here:
-- getToken(rawStructInstance, token)
assert(type(b) == "string", "Invalid argument 2 to getToken, must be a string")
local unit, token = from, b
return getRaceCasteTokenCore(df.global.world.raws.creatures.all[unit.race], unit.caste, token)
elseif df.is_instance(df.plant_raw, from) then
-- Signatures from here:
-- getToken(plantDefinition, growthNumber, token)
-- getToken(plantDefinition, growthString, token)
return getTokenArg1PlantDefinition(from, b, c)
elseif df.is_instance(df.plant, from) then
-- Signatures from here:
-- getToken(rawStructInstance, token)
assert(type(b) == "string", "Invalid argument 2 to getToken, must be a string")
local plantDefinition, plantGrowthNumber, token = df.global.world.raws.plants.all[from.material], -1, b
return getPlantGrowthTokenCore(plantDefinition, plantGrowthNumber, token)
elseif df.is_instance(df.item_plantst, from) then
-- Signatures from here:
-- getToken(rawStructInstance, token)
local matInfo = dfhack.matinfo.decode(from)
if matInfo.mode ~= "plant" then return false end
assert(type(b) == "string", "Invalid argument 2 to getToken, must be a string")
local plantDefinition, plantGrowthNumber, token = matInfo.plant, -1, b
return getPlantGrowthTokenCore(plantDefinition, plantGrowthNumber, token)
elseif df.is_instance(df.item_plant_growthst, from) then
-- Signatures from here:
-- getToken(rawStructInstance, token)
local matInfo = dfhack.matinfo.decode(from)
if matInfo.mode ~= "plant" then return false end
assert(type(b) == "string", "Invalid argument 2 to getToken, must be a string")
local plantDefinition, plantGrowthNumber, token = matInfo.plant, from.growth_print, b
return getPlantGrowthTokenCore(plantDefinition, plantGrowthNumber, token)
else
-- Signatures from here:
-- getToken(rawStruct, token)
-- getToken(rawStructInstance, token)
return getTokenArg1Else(from, b)
end
end
return _ENV