diff --git a/docs/Lua API.rst b/docs/Lua API.rst index a61264c86..4615aaf4f 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3198,6 +3198,80 @@ Predefined instance methods: 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 ================== diff --git a/docs/changelog.txt b/docs/changelog.txt index adc25327a..8495f6717 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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.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)`` +- 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.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 diff --git a/library/lua/custom-raw-tokens.lua b/library/lua/custom-raw-tokens.lua new file mode 100644 index 000000000..6c19deb72 --- /dev/null +++ b/library/lua/custom-raw-tokens.lua @@ -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