Merge branch 'develop' into Bumber64-patch-2

develop
Ryan Williams 2022-05-01 23:42:21 -07:00 committed by GitHub
commit 621ccef996
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 907 additions and 167 deletions

@ -29,6 +29,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install \
ccache \
libgtk2.0-0 \
libncursesw5 \
libsdl-image1.2-dev \
@ -60,6 +61,11 @@ jobs:
with:
path: ~/DF
key: ${{ steps.env_setup.outputs.df_version }}
- name: Fetch ccache
uses: actions/cache@v2
with:
path: ~/.ccache
key: ccache-${{ matrix.os }}-gcc-${{ matrix.gcc }}
- name: Download DF
run: |
sh ci/download-df.sh
@ -121,7 +127,7 @@ jobs:
submodules: true
- name: Build docs
run: |
sphinx-build -W --keep-going -j3 . docs/html
sphinx-build -W --keep-going -j3 --color . docs/html
- name: Upload docs
uses: actions/upload-artifact@v1
with:

1
.gitignore vendored

@ -72,5 +72,4 @@ tags
.idea
# external plugins
/plugins/external/
/plugins/CMakeLists.custom.txt

@ -32,4 +32,12 @@ repos:
exclude_types:
- json
# specific to dfhack:
- repo: local
hooks:
- id: authors-rst
name: Check Authors.rst
language: python
entry: python3 ci/authors-rst.py
files: docs/Authors\.rst
pass_filenames: false
exclude: '^(depends/|data/examples/.*\.json$|.*\.diff$)'

@ -3,29 +3,31 @@
# configuration here is useful for any fort! Feed free to edit or override
# to your liking.
# Disallow cooking of otherwise useful item types
on-new-fortress ban-cooking tallow; ban-cooking honey; ban-cooking oil; ban-cooking seeds; ban-cooking brew; ban-cooking fruit; ban-cooking mill; ban-cooking thread; ban-cooking milk; ban-cooking booze
# Uncomment this next line if you want buildingplan (and quickfort) to use only
# blocks for construction. If you do uncomment, be sure to bring some blocks
# with you for starting workshops!
# blocks (not bars or logs) for constructions and workshops. If you do
# uncomment, be sure to bring some blocks with you for starting workshops!
#on-new-fortress buildingplan set boulders false; buildingplan set logs false
# Disable cooking of useful item types when you start a new fortress.
on-new-fortress ban-cooking tallow; ban-cooking honey; ban-cooking oil; ban-cooking seeds; ban-cooking brew; ban-cooking fruit; ban-cooking mill; ban-cooking thread; ban-cooking milk; ban-cooking booze
# Show a warning dialog when units are starving
repeat -name warn-starving -time 10 -timeUnits days -command [ warn-starving ]
repeat -name burial -time 7 -timeUnits days -command [ burial -pets ]
# Force dwarves to drop tattered clothing instead of clinging to the scraps
repeat -name cleanowned -time 1 -timeUnits months -command [ cleanowned X ]
repeat -name clean -time 1 -timeUnits months -command [ clean all ]
repeat -name feeding-timers -time 1 -timeUnits months -command [ fix/feeding-timers ]
repeat -name stuckdoors -time 1 -timeUnits months -command [ fix/stuckdoors ]
# Automatically enqueue orders to shear and milk animals
repeat -name autoShearCreature -time 14 -timeUnits days -command [ workorder ShearCreature ]
repeat -name autoMilkCreature -time 14 -timeUnits days -command [ workorder "{\"job\":\"MilkCreature\",\"item_conditions\":[{\"condition\":\"AtLeast\",\"value\":2,\"flags\":[\"empty\"],\"item_type\":\"BUCKET\"}]}" ]
# Fulfill high-volume orders before slower once-daily orders
repeat -name orders-sort -time 1 -timeUnits days -command [ orders sort ]
tweak fast-heat 100
tweak do-job-now
fix/blood-del enable
# Don't let caravans bring barrels of blood and other useless liquids
fix/blood-del
# manages crop assignment for farm plots
# Manages crop assignment for farm plots
enable autofarm
autofarm default 30
autofarm threshold 150 GRASS_TAIL_PIG
@ -37,7 +39,8 @@ enable automelt
enable tailor
tailor enable
# auto-assigns nesting birds to nestbox zones
# auto-assigns nesting birds to nestbox zones and protects fertile eggs from
# being cooked/eaten
enable zone nestboxes
autonestbox start
@ -77,7 +80,7 @@ on-new-fortress autobutcher target 50 50 14 2 BIRD_GOOSE
on-new-fortress autobutcher target 2 2 4 2 ALPACA SHEEP LLAMA
# pigs give milk and meat and are zero-maintenance.
on-new-fortress autobutcher target 5 5 6 2 PIG
# generally unprofitable animals
# butcher all unprofitable animals
on-new-fortress autobutcher target 0 0 0 0 HORSE YAK DONKEY WATER_BUFFALO GOAT CAVY BIRD_DUCK BIRD_GUINEAFOWL
# start it up!
on-new-fortress autobutcher start; autobutcher watch all; autobutcher autowatch

@ -34,6 +34,9 @@ keybinding add Ctrl-K autodump-destroy-item
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# gui/quickfort script - apply pre-made blueprints to the map
keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort
# gui/rename script - rename units and buildings
keybinding add Ctrl-Shift-N gui/rename
keybinding add Ctrl-Shift-T "gui/rename unit-profession"
@ -155,9 +158,6 @@ keybinding add Alt-W@dfhack/lua/status_overlay "gui/workflow status"
# autobutcher front-end
keybinding add Shift-B@pet/List/Unit "gui/autobutcher"
# assign weapon racks to squads so that they can be used
keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack
# view pathable tiles from active cursor
keybinding add Alt-Shift-P@dwarfmode/LookAround gui/pathable
@ -186,9 +186,6 @@ tweak military-stable-assign
# in same list, color units already assigned to squads in brown & green
tweak military-color-assigned
# remove inverse dependency of squad training speed on unit list size and use more sparring
# tweak military-training
# make crafted cloth items wear out with time like in old versions (bug 6003)
tweak craft-age-wear
@ -210,17 +207,22 @@ tweak hotkey-clear
# Allows lowercase letters in embark profile names, and allows exiting the name prompt without saving
tweak embark-profile-name
# Reduce performance impact of temperature changes
tweak fast-heat 100
# Misc. UI tweaks
tweak block-labors # Prevents labors that can't be used from being toggled
tweak burrow-name-cancel
tweak cage-butcher
tweak civ-view-agreement
tweak do-job-now
tweak eggs-fertile
tweak fps-min
tweak hide-priority
tweak kitchen-prefs-all
tweak kitchen-prefs-empty
tweak max-wheelbarrow
tweak partial-items
tweak shift-8-scroll
tweak stone-status-all
tweak title-start-rename
@ -289,5 +291,6 @@ gui/load-screen enable
# Extra DFHack command files #
##############################
# Run commands in this file when a world loads
sc-script add SC_WORLD_LOADED onLoad.init-example
# Create a file named "onLoad.init" to run commands when a world is loaded
# and/or create a file named "onMapLoad.init" to run commands when a map is
# loaded. See the hack/examples/init/ directory for useful pre-made init files.

@ -34,6 +34,7 @@ brndd brndd burneddi
Caldfir caldfir
Carter Bray Qartar
Chris Dombroski cdombroski
Chris Parsons chrismdp
Clayton Hughes
Clément Vuchener cvuchener
daedsidog daedsidog
@ -47,6 +48,7 @@ Deon
DoctorVanGogh DoctorVanGogh
Donald Ruegsegger hashaash
doomchild doomchild
DwarvenM DwarvenM
ElMendukol ElMendukol
enjia2000
Eric Wald eswald
@ -59,6 +61,7 @@ Gabe Rau gaberau
gchristopher gchristopher
George Murray GitOnUp
grubsteak grubsteak
Guilherme Abraham GuilhermeAbraham
Harlan Playford playfordh
Hayati Ayguen hayguen
Herwig Hochleitner bendlas
@ -112,6 +115,7 @@ napagokc napagokc
Neil Little nmlittle
Nick Rart nickrart comestible
Nicolas Ayala nicolasayala
Nik Nyby nikolas
Nikolay Amiantov abbradar
nocico nocico
Omniclasm
@ -123,6 +127,7 @@ Paul Fenwick pjf
PeridexisErrant PeridexisErrant
Petr Mrázek peterix
Pfhreak Pfhreak
Pierre Lulé plule
Pierre-David Bélanger pierredavidbelanger
potato
Priit Laes plaes
@ -173,6 +178,7 @@ Theo Kalfas teolandon
therahedwig therahedwig
ThiagoLira ThiagoLira
thurin thurin
Tim Siegel softmoth
Tim Walberg twalberg
Timothy Collett danaris
Timur Kelman TymurGubayev

@ -2667,6 +2667,12 @@ environment by the mandatory init file dfhack.lua:
Walks a sequence of dereferences, which may be represented by numbers or strings.
Returns *nil* if any of obj or indices is *nil*, or a numeric index is out of array bounds.
* ``ensure_key(t, key[, default_value])``
If the Lua table ``t`` doesn't include the specified ``key``, ``t[key]`` is
set to the value of ``default_value``, which defaults to ``{}`` if not set.
The new or existing value of ``t[key]`` is then returned.
.. _lua-string:
String class extentions
@ -3216,6 +3222,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
==================
@ -3800,6 +3880,14 @@ It has the following attributes:
:auto_width: Sets self.frame.w from the text width.
:on_click: A callback called when the label is clicked (optional)
:on_rclick: A callback called when the label is right-clicked (optional)
:scroll_keys: Specifies which keys the label should react to as a table. Default is ``STANDARDSCROLL`` (up or down arrows, page up or down).
:show_scroll_icons: Controls scroll icons' behaviour: ``false`` for no icons, ``'right'`` or ``'left'`` for
icons next to the text in an additional column (``frame_inset`` is adjusted to have ``.r`` or ``.l`` greater than ``0``),
``nil`` same as ``'right'`` but changes ``frame_inset`` only if a scroll icon is actually necessary
(if ``getTextHeight()`` is greater than ``frame_body.height``). Default is ``nil``.
:up_arrow_icon: The symbol for scroll up arrow. Default is ``string.char(24)`` (``↑``).
:down_arrow_icon: The symbol for scroll down arrow. Default is ``string.char(25)`` (``↓``).
:scroll_icon_pen: Specifies the pen for scroll icons. Default is ``COLOR_LIGHTCYAN``.
The text itself is represented as a complex structure, and passed
to the object via the ``text`` argument of the constructor, or via
@ -4337,7 +4425,7 @@ Native functions (exported to Lua)
adds a number to the sequence
- ``ShuffleSequence(rngID, seqID)``
- ``ShuffleSequence(seqID, rngID)``
shuffles the number sequence
@ -4400,7 +4488,7 @@ Lua plugin classes
``bool_distribution``
~~~~~~~~~~~~~~~~~~~~~
- ``init(min, max)``: constructor
- ``init(chance)``: constructor
- ``next(id)``: returns next boolean in the distribution
- ``id``: engine ID to pass to native function
@ -4413,6 +4501,41 @@ Lua plugin classes
- ``shuffle()``: shuffles the sequence of numbers
- ``next()``: returns next number in the sequence
Usage
-----
The basic idea is you create a number distribution which you generate random numbers along. The C++ relies
on engines keeping state information to determine the next number along the distribution.
You're welcome to try and (ab)use this knowledge for your RNG purposes.
Example::
local rng = require('plugins.cxxrandom')
local norm_dist = rng.normal_distribution(6820,116) // avg, stddev
local engID = rng.MakeNewEngine(0)
-- somewhat reminiscent of the C++ syntax
print(norm_dist:next(engID))
-- a bit more streamlined
local cleanup = true --delete engine on cleanup
local number_generator = rng.crng:new(engID, cleanup, norm_dist)
print(number_generator:next())
-- simplified
print(rng.rollNormal(engID,6820,116))
The number sequences are much simpler. They're intended for where you need to randomly generate an index, perhaps in a loop for an array. You technically don't need an engine to use it, if you don't mind never shuffling.
Example::
local rng = require('plugins.cxxrandom')
local g = rng.crng:new(rng.MakeNewEngine(0), true, rng.num_sequence:new(0,table_size))
g:shuffle()
for _ = 1, table_size do
func(array[g:next()])
end
dig-now
=======

@ -159,6 +159,7 @@ Subcommands that persist until disabled or DF quits:
i.e. stop the rightmost list of the Positions page of the military
screen from constantly resetting to the top.
:nestbox-color: Fixes the color of built nestboxes
:partial-items: Displays percentages on partially-consumed items such as hospital cloth
:reaction-gloves: Fixes reactions to produce gloves in sets with correct handedness (:bug:`6273`)
:shift-8-scroll: Gives Shift-8 (or :kbd:`*`) priority when scrolling menus, instead of scrolling the map
:stable-cursor: Saves the exact cursor position between t/q/k/d/b/etc menus of fortress mode, if the
@ -696,7 +697,7 @@ also tries to have dwarves specialize in specific skills.
The key is that, for almost all labors, once a dwarf begins a job it will finish that
job even if the associated labor is removed. Autolabor therefore frequently checks
which dwarf or dwarves should take new jobs for that labor, and sets labors accordingly.
Labors with equiptment (mining, hunting, and woodcutting), which are abandoned
Labors with equipment (mining, hunting, and woodcutting), which are abandoned
if labors change mid-job, are handled slightly differently to minimise churn.
.. warning::

@ -75,10 +75,11 @@ from other (non-C++) languages, including:
- `RemoteClientDF-Net <https://github.com/RosaryMala/RemoteClientDF-Net>`_ for C#
- `dfhackrpc <https://github.com/BenLubar/dfhackrpc>`_ for Go
- `dfhack-remote <https://github.com/alexchandel/dfhack-remote>`_ for JavaScript
- `dfhack-remote <https://github.com/alexchandel/dfhack-remote>`__ for JavaScript
- `dfhack-client-qt <https://github.com/cvuchener/dfhack-client-qt>`_ for C++ with Qt
- `dfhack-client-python <https://github.com/McArcady/dfhack-client-python>`_ for Python (adapted from :forums:`"Blendwarf" <178089>`)
- `dfhack-client-java <https://github.com/McArcady/dfhack-client-java>`_ for Java
- `dfhack-remote <https://docs.rs/dfhack-remote/latest/dfhack_remote/index.html>`__ for Rust
Protocol description

@ -461,7 +461,7 @@ endif()
install(FILES xml/symbols.xml
DESTINATION ${DFHACK_DATA_DESTINATION})
# install the example autoexec file
install(FILES ../dfhack.init-example ../onLoad.init-example
install(FILES ../dfhack.init-example
DESTINATION ${DFHACK_BINARY_DESTINATION})
install(TARGETS dfhack-run dfhack-client binpatch

@ -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

@ -385,6 +385,13 @@ function safe_index(obj,idx,...)
end
end
function ensure_key(t, key, default_value)
if t[key] == nil then
t[key] = (default_value ~= nil) and default_value or {}
end
return t[key]
end
-- String class extentions
-- prefix is a literal string, not a pattern
@ -432,6 +439,7 @@ end
-- multiple lines. If width is not specified, 72 is used.
function string:wrap(width)
width = width or 72
if width <= 0 then error('expected width > 0; got: '..tostring(width)) end
local wrapped_text = {}
for line in self:gmatch('[^\n]*') do
local line_start_pos = 1

@ -96,7 +96,7 @@ function compute_frame_rect(wavail,havail,spec,xgap,ygap)
return rect
end
local function parse_inset(inset)
function parse_inset(inset)
local l,r,t,b
if type(inset) == 'table' then
l,r = inset.l or inset.x or 0, inset.r or inset.x or 0

@ -69,22 +69,26 @@ function MessageBox:onInput(keys)
end
function showMessage(title, text, tcolor, on_close)
MessageBox{
local mb = MessageBox{
frame_title = title,
text = text,
text_pen = tcolor,
on_close = on_close
}:show()
}
mb:show()
return mb
end
function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
MessageBox{
local mb = MessageBox{
frame_title = title,
text = text,
text_pen = tcolor,
on_accept = on_accept,
on_cancel = on_cancel,
}:show()
}
mb:show()
return mb
end
InputBox = defclass(InputBox, MessageBox)
@ -133,7 +137,7 @@ function InputBox:onInput(keys)
end
function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
InputBox{
local ib = InputBox{
frame_title = title,
text = text,
text_pen = tcolor,
@ -141,7 +145,9 @@ function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_wi
on_input = on_input,
on_cancel = on_cancel,
frame_width = min_width,
}:show()
}
ib:show()
return ib
end
ListBox = defclass(ListBox, MessageBox)
@ -201,7 +207,7 @@ function ListBox:init(info)
on_submit2 = on_submit2,
frame = { l = 0, r = 0},
frame_inset = self.list_frame_inset,
row_height = info.row_height,
row_height = self.row_height,
}
}
end
@ -232,7 +238,7 @@ function ListBox:onInput(keys)
end
function showListPrompt(title, text, tcolor, choices, on_select, on_cancel, min_width, filter)
ListBox{
local lb = ListBox{
frame_title = title,
text = text,
text_pen = tcolor,
@ -241,7 +247,9 @@ function showListPrompt(title, text, tcolor, choices, on_select, on_cancel, min_
on_cancel = on_cancel,
frame_width = min_width,
with_filter = filter,
}:show()
}
lb:show()
return lb
end
return _ENV

@ -443,11 +443,7 @@ MenuOverlay.ATTRS {
sidebar_mode = DEFAULT_NIL,
}
function MenuOverlay:computeFrame(parent_rect)
return self.df_layout.menu, gui.inset_frame(self.df_layout.menu, self.frame_inset)
end
function MenuOverlay:onAboutToShow(parent)
function MenuOverlay:init()
if not dfhack.isMapLoaded() then
-- sidebar menus are only valid when a fort map is loaded
error('A fortress map must be loaded.')
@ -471,7 +467,13 @@ function MenuOverlay:onAboutToShow(parent)
enterSidebarMode(self.sidebar_mode)
end
end
function MenuOverlay:computeFrame(parent_rect)
return self.df_layout.menu, gui.inset_frame(self.df_layout.menu, self.frame_inset)
end
function MenuOverlay:onAboutToShow(parent)
self:updateLayout()
if not self.df_layout.menu then
error("The menu panel of dwarfmode is not visible")
@ -502,6 +504,55 @@ function MenuOverlay:render(dc)
MenuOverlay.super.render(self, dc)
end
end
-- Framework for managing rendering over the map area. This function is intended
-- to be called from a subclass's onRenderBody() function.
--
-- get_overlay_char_fn takes a coordinate position and an is_cursor boolean and
-- returns the char to render at that position and, optionally, the foreground
-- and background colors to use to draw the char. If nothing should be rendered
-- at that position, the function should return nil. If no foreground color is
-- specified, it defaults to COLOR_GREEN. If no background color is specified,
-- it defaults to COLOR_BLACK.
--
-- bounds_rect has elements {x1, x2, y1, y2} in global map coordinates (not
-- screen coordinates). The rect is intersected with the visible map viewport to
-- get the range over which get_overlay_char_fn is called. If bounds_rect is not
-- specified, the entire viewport is scanned.
--
-- example call from a subclass:
-- function MyMenuOverlaySubclass:onRenderBody()
-- local function get_overlay_char(pos)
-- return safe_index(self.overlay_chars, pos.z, pos.y, pos.x), COLOR_RED
-- end
-- self:renderMapOverlay(get_overlay_char, self.overlay_bounds)
-- end
function MenuOverlay:renderMapOverlay(get_overlay_char_fn, bounds_rect)
local vp = self:getViewport()
local rect = gui.ViewRect{rect=vp,
clip_view=bounds_rect and gui.ViewRect{rect=bounds_rect} or nil}
-- nothing to do if the viewport is completely separate from the bounds_rect
if rect:isDefunct() then return end
local dc = gui.Painter.new(self.df_layout.map)
local z = df.global.window_z
local cursor = getCursorPos()
for y=rect.clip_y1,rect.clip_y2 do
for x=rect.clip_x1,rect.clip_x2 do
local pos = xyz2pos(x, y, z)
local overlay_char, fg_color, bg_color = get_overlay_char_fn(
pos, same_xy(cursor, pos))
if not overlay_char then goto continue end
local stile = vp:tileToScreen(pos)
dc:map(true):seek(stile.x, stile.y):
pen(fg_color or COLOR_GREEN, bg_color or COLOR_BLACK):
char(overlay_char):map(false)
::continue::
end
end
end
--fakes a "real" workshop sidebar menu, but on exactly selected workshop
WorkshopOverlay = defclass(WorkshopOverlay, MenuOverlay)
WorkshopOverlay.focus_path="WorkshopOverlay"

@ -412,7 +412,12 @@ Label.ATTRS{
auto_width = false,
on_click = DEFAULT_NIL,
on_rclick = DEFAULT_NIL,
--
scroll_keys = STANDARDSCROLL,
show_scroll_icons = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false
up_arrow_icon = string.char(24),
down_arrow_icon = string.char(25),
scroll_icon_pen = COLOR_LIGHTCYAN,
}
function Label:init(args)
@ -435,6 +440,39 @@ function Label:setText(text)
end
end
function Label:update_scroll_inset()
if self.show_scroll_icons == nil then
self._show_scroll_icons = self:getTextHeight() > self.frame_body.height and 'right' or false
else
self._show_scroll_icons = self.show_scroll_icons
end
if self._show_scroll_icons then
-- here self._show_scroll_icons can only be either
-- 'left' or any true value which we interpret as right
local l,t,r,b = gui.parse_inset(self.frame_inset)
if self._show_scroll_icons == 'left' and l <= 0 then
l = 1
elseif r <= 0 then
r = 1
end
self.frame_inset = {l=l,t=t,r=r,b=b}
end
end
function Label:render_scroll_icons(dc, x, y1, y2)
if self.start_line_num ~= 1 then
dc:seek(x, y1):char(self.up_arrow_icon, self.scroll_icon_pen)
end
local last_visible_line = self.start_line_num + self.frame_body.height - 1
if last_visible_line < self:getTextHeight() then
dc:seek(x, y2):char(self.down_arrow_icon, self.scroll_icon_pen)
end
end
function Label:postComputeFrame()
self:update_scroll_inset()
end
function Label:preUpdateLayout()
if self.auto_width then
self.frame = self.frame or {}
@ -465,6 +503,21 @@ function Label:onRenderBody(dc)
render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self))
end
function Label:onRenderFrame(dc, rect)
if self._show_scroll_icons
and self:getTextHeight() > self.frame_body.height
then
local x = self._show_scroll_icons == 'left'
and self.frame_body.x1-dc.x1-1
or self.frame_body.x2-dc.x1+1
self:render_scroll_icons(dc,
x,
self.frame_body.y1-dc.y1,
self.frame_body.y2-dc.y1
)
end
end
function Label:scroll(nlines)
local n = self.start_line_num + nlines
n = math.min(n, self:getTextHeight() - self.frame_body.height + 1)
@ -505,7 +558,8 @@ WrappedLabel.ATTRS{
}
function WrappedLabel:getWrappedText(width)
if not self.text_to_wrap then return nil end
-- 0 width can happen if the parent has 0 width
if not self.text_to_wrap or width <= 0 then return nil end
local text_to_wrap = getval(self.text_to_wrap)
if type(text_to_wrap) == 'table' then
text_to_wrap = table.concat(text_to_wrap, NEWLINE)

@ -1 +1 @@
Subproject commit b8d48430aa13570a668464ce40294a589f1f0b1f
Subproject commit 5fe90d06ebad4f9686545b7c961c2c07b8bcb799

@ -1 +0,0 @@
repeat -name warn-starving -time 10 -timeUnits days -command [ warn-starving ]

@ -189,6 +189,17 @@ if(BUILD_SKELETON)
add_subdirectory(skeleton)
endif()
macro(subdirlist result subdir)
file(GLOB children ABSOLUTE ${subdir}/ ${subdir}/*/)
set(dirlist "")
foreach(child ${children})
if(IS_DIRECTORY ${child})
file(RELATIVE_PATH child ${CMAKE_CURRENT_SOURCE_DIR}/${subdir} ${child})
list(APPEND dirlist ${child})
endif()
endforeach()
set(${result} ${dirlist})
endmacro()
# To add "external" plugins without committing them to the DFHack repo:
#
@ -204,7 +215,7 @@ endif()
# 4. build DFHack as normal. The plugins you added will be built as well.
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/CMakeLists.txt")
file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/external/CMakeLists.txt"
set(content_str
"# Add external plugins here - this file is ignored by git
# Recommended: use add_subdirectory() for folders that you have created within
@ -212,9 +223,14 @@ if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/CMakeLists.txt")
# See the end of /plugins/CMakeLists.txt for more details.
")
subdirlist(SUBDIRS external)
foreach(subdir ${SUBDIRS})
set(content_str "${content_str}add_subdirectory(${subdir})\n")
endforeach()
file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/external/CMakeLists.txt" ${content_str})
endif()
include("${CMAKE_CURRENT_SOURCE_DIR}/external/CMakeLists.txt")
add_subdirectory(external)
# for backwards compatibility
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.custom.txt")

@ -37,94 +37,82 @@ DFHACK_PLUGIN("cxxrandom");
#define PLUGIN_VERSION 2.0
color_ostream *cout = nullptr;
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands)
{
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) {
cout = &out;
return CR_OK;
}
DFhackCExport command_result plugin_shutdown (color_ostream &out)
{
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
return CR_OK;
}
#define EK_ID_BASE (1ll << 40)
class EnginesKeeper
{
private:
EnginesKeeper() {}
std::unordered_map<uint16_t, std::mt19937_64> m_engines;
uint16_t counter = 0;
EnginesKeeper() = default;
std::unordered_map<uint64_t, std::mt19937_64> m_engines;
uint64_t id_counter = EK_ID_BASE;
public:
static EnginesKeeper& Instance()
{
static EnginesKeeper& Instance() {
static EnginesKeeper instance;
return instance;
}
uint16_t NewEngine( uint64_t seed )
{
uint64_t NewEngine( uint64_t seed ) {
auto id = ++id_counter;
CHECK_INVALID_ARGUMENT(m_engines.count(id) == 0);
std::mt19937_64 engine( seed != 0 ? seed : std::chrono::system_clock::now().time_since_epoch().count() );
m_engines[++counter] = engine;
return counter;
m_engines[id] = engine;
return id;
}
void DestroyEngine( uint16_t id )
{
void DestroyEngine( uint64_t id ) {
m_engines.erase( id );
}
void NewSeed( uint16_t id, uint64_t seed )
{
void NewSeed( uint64_t id, uint64_t seed ) {
CHECK_INVALID_ARGUMENT( m_engines.find( id ) != m_engines.end() );
m_engines[id].seed( seed != 0 ? seed : std::chrono::system_clock::now().time_since_epoch().count() );
}
std::mt19937_64& RNG( uint16_t id )
{
std::mt19937_64& RNG( uint64_t id ) {
CHECK_INVALID_ARGUMENT( m_engines.find( id ) != m_engines.end() );
return m_engines[id];
}
};
uint16_t GenerateEngine( uint64_t seed )
{
uint64_t GenerateEngine( uint64_t seed ) {
return EnginesKeeper::Instance().NewEngine( seed );
}
void DestroyEngine( uint16_t id )
{
void DestroyEngine( uint64_t id ) {
EnginesKeeper::Instance().DestroyEngine( id );
}
void NewSeed( uint16_t id, uint64_t seed )
{
void NewSeed( uint64_t id, uint64_t seed ) {
EnginesKeeper::Instance().NewSeed( id, seed );
}
int rollInt(uint16_t id, int min, int max)
{
int rollInt(uint64_t id, int min, int max) {
std::uniform_int_distribution<int> ND(min, max);
return ND(EnginesKeeper::Instance().RNG(id));
}
double rollDouble(uint16_t id, double min, double max)
{
double rollDouble(uint64_t id, double min, double max) {
std::uniform_real_distribution<double> ND(min, max);
return ND(EnginesKeeper::Instance().RNG(id));
}
double rollNormal(uint16_t id, double mean, double stddev)
{
double rollNormal(uint64_t id, double mean, double stddev) {
std::normal_distribution<double> ND(mean, stddev);
return ND(EnginesKeeper::Instance().RNG(id));
}
bool rollBool(uint16_t id, float p)
{
bool rollBool(uint64_t id, float p) {
std::bernoulli_distribution ND(p);
return ND(EnginesKeeper::Instance().RNG(id));
}
@ -137,118 +125,104 @@ private:
std::vector<int64_t> m_numbers;
public:
NumberSequence(){}
NumberSequence( int64_t start, int64_t end )
{
for( int64_t i = start; i <= end; ++i )
{
NumberSequence( int64_t start, int64_t end ) {
for( int64_t i = start; i <= end; ++i ) {
m_numbers.push_back( i );
}
}
void Add( int64_t num ) { m_numbers.push_back( num ); }
void Reset() { m_numbers.clear(); }
int64_t Next()
{
if(m_position >= m_numbers.size())
{
void Reset() { m_numbers.clear(); }
int64_t Next() {
if(m_position >= m_numbers.size()) {
m_position = 0;
}
return m_numbers[m_position++];
}
void Shuffle( uint16_t id )
{
std::shuffle( std::begin( m_numbers ), std::end( m_numbers ), EnginesKeeper::Instance().RNG( id ) );
void Shuffle( uint64_t engID ) {
std::shuffle( std::begin( m_numbers ), std::end( m_numbers ), EnginesKeeper::Instance().RNG(engID));
}
void Print()
{
for( auto v : m_numbers )
{
void Print() {
for( auto v : m_numbers ) {
cout->print( "%" PRId64 " ", v );
}
}
};
#define SK_ID_BASE 0
class SequenceKeeper
{
private:
SequenceKeeper() {}
std::unordered_map<uint16_t, NumberSequence> m_sequences;
uint16_t counter = 0;
SequenceKeeper() = default;
std::unordered_map<uint64_t, NumberSequence> m_sequences;
uint64_t id_counter = SK_ID_BASE;
public:
static SequenceKeeper& Instance()
{
static SequenceKeeper& Instance() {
static SequenceKeeper instance;
return instance;
}
uint16_t MakeNumSequence( int64_t start, int64_t end )
{
m_sequences[++counter] = NumberSequence( start, end );
return counter;
}
uint16_t MakeNumSequence()
{
m_sequences[++counter] = NumberSequence();
return counter;
}
void DestroySequence( uint16_t id )
{
m_sequences.erase( id );
}
void AddToSequence( uint16_t id, int64_t num )
{
CHECK_INVALID_ARGUMENT( m_sequences.find( id ) != m_sequences.end() );
m_sequences[id].Add( num );
}
void Shuffle( uint16_t id, uint16_t rng_id )
{
CHECK_INVALID_ARGUMENT( m_sequences.find( id ) != m_sequences.end() );
m_sequences[id].Shuffle( rng_id );
}
int64_t NextInSequence( uint16_t id )
{
CHECK_INVALID_ARGUMENT( m_sequences.find( id ) != m_sequences.end() );
return m_sequences[id].Next();
}
void PrintSequence( uint16_t id )
{
CHECK_INVALID_ARGUMENT( m_sequences.find( id ) != m_sequences.end() );
auto seq = m_sequences[id];
uint64_t MakeNumSequence( int64_t start, int64_t end ) {
auto id = ++id_counter;
CHECK_INVALID_ARGUMENT(m_sequences.count(id) == 0);
m_sequences[id] = NumberSequence(start, end);
return id;
}
uint64_t MakeNumSequence() {
auto id = ++id_counter;
CHECK_INVALID_ARGUMENT(m_sequences.count(id) == 0);
m_sequences[id] = NumberSequence();
return id;
}
void DestroySequence( uint64_t seqID ) {
m_sequences.erase(seqID);
}
void AddToSequence(uint64_t seqID, int64_t num ) {
CHECK_INVALID_ARGUMENT(m_sequences.find(seqID) != m_sequences.end());
m_sequences[seqID].Add(num);
}
void Shuffle(uint64_t seqID, uint64_t engID ) {
uint64_t sid = seqID >= SK_ID_BASE ? seqID : engID;
uint64_t eid = engID >= EK_ID_BASE ? engID : seqID;
CHECK_INVALID_ARGUMENT(m_sequences.find(sid) != m_sequences.end());
m_sequences[sid].Shuffle(eid);
}
int64_t NextInSequence( uint64_t seqID ) {
CHECK_INVALID_ARGUMENT(m_sequences.find(seqID) != m_sequences.end());
return m_sequences[seqID].Next();
}
void PrintSequence( uint64_t seqID ) {
CHECK_INVALID_ARGUMENT(m_sequences.find(seqID) != m_sequences.end());
auto seq = m_sequences[seqID];
seq.Print();
}
};
uint16_t MakeNumSequence( int64_t start, int64_t end )
{
if( start == end )
{
uint64_t MakeNumSequence( int64_t start, int64_t end ) {
if (start == end) {
return SequenceKeeper::Instance().MakeNumSequence();
}
return SequenceKeeper::Instance().MakeNumSequence( start, end );
return SequenceKeeper::Instance().MakeNumSequence(start, end);
}
void DestroyNumSequence( uint16_t id )
{
SequenceKeeper::Instance().DestroySequence( id );
void DestroyNumSequence( uint64_t seqID ) {
SequenceKeeper::Instance().DestroySequence(seqID);
}
void AddToSequence( uint16_t id, int64_t num )
{
SequenceKeeper::Instance().AddToSequence( id, num );
void AddToSequence(uint64_t seqID, int64_t num ) {
SequenceKeeper::Instance().AddToSequence(seqID, num);
}
void ShuffleSequence( uint16_t rngID, uint16_t id )
{
SequenceKeeper::Instance().Shuffle( id, rngID );
void ShuffleSequence(uint64_t seqID, uint64_t engID ) {
SequenceKeeper::Instance().Shuffle(seqID, engID);
}
int64_t NextInSequence( uint16_t id )
{
return SequenceKeeper::Instance().NextInSequence( id );
int64_t NextInSequence( uint64_t seqID ) {
return SequenceKeeper::Instance().NextInSequence(seqID);
}
void DebugSequence( uint16_t id )
{
SequenceKeeper::Instance().PrintSequence( id );
void DebugSequence( uint64_t seqID ) {
SequenceKeeper::Instance().PrintSequence(seqID);
}

@ -0,0 +1,2 @@
*
!.gitignore

@ -151,8 +151,8 @@ bool_distribution = {}
function bool_distribution:new(chance)
local o = {}
self.__index = self
if type(min) ~= 'number' or type(max) ~= 'number' then
error("Invalid arguments in bool_distribution construction. min and max must be numbers.")
if type(chance) ~= 'number' or chance < 0 or chance > 1 then
error("Invalid arguments in bool_distribution construction. chance must be a number between 0.0 and 1.0 (both included).")
end
o.p = chance
setmetatable(o,self)
@ -208,7 +208,7 @@ function num_sequence:shuffle()
if self.rngID == 'nil' then
error("Add num_sequence object to crng as distribution, before attempting to shuffle.")
end
ShuffleSequence(self.rngID, self.seqID)
ShuffleSequence(self.seqID, self.rngID)
end
return _ENV

@ -49,11 +49,14 @@
#include "df/item_glovesst.h"
#include "df/item_shoesst.h"
#include "df/item_pantsst.h"
#include "df/item_drinkst.h"
#include "df/item_globst.h"
#include "df/item_liquid_miscst.h"
#include "df/item_powder_miscst.h"
#include "df/item_barst.h"
#include "df/item_threadst.h"
#include "df/item_clothst.h"
#include "df/item_sheetst.h"
#include "df/spatter.h"
#include "df/layer_object.h"
#include "df/reaction.h"
@ -68,6 +71,7 @@
#include "df/job.h"
#include "df/general_ref_building_holderst.h"
#include "df/unit_health_info.h"
#include "df/caste_body_info.h"
#include "df/activity_entry.h"
#include "df/activity_event_combat_trainingst.h"
#include "df/activity_event_individual_skill_drillst.h"
@ -101,14 +105,15 @@
#include "tweaks/kitchen-prefs-empty.h"
#include "tweaks/max-wheelbarrow.h"
#include "tweaks/military-assign.h"
#include "tweaks/pausing-fps-counter.h"
#include "tweaks/nestbox-color.h"
#include "tweaks/partial-items.h"
#include "tweaks/pausing-fps-counter.h"
#include "tweaks/reaction-gloves.h"
#include "tweaks/shift-8-scroll.h"
#include "tweaks/stable-cursor.h"
#include "tweaks/stone-status-all.h"
#include "tweaks/title-start-rename.h"
#include "tweaks/tradereq-pet-gender.h"
#include "tweaks/reaction-gloves.h"
using std::set;
using std::vector;
@ -244,6 +249,8 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector <Plugi
" Preserve list order and cursor position when assigning to squad,\n"
" i.e. stop the rightmost list of the Positions page of the military\n"
" screen from constantly jumping to the top.\n"
" tweak partial-items [disable]\n"
" Displays percentages on partially-consumed items such as hospital cloth\n"
" tweak pausing-fps-counter [disable]\n"
" Replace fortress mode FPS counter with one that stops counting \n"
" when paused.\n"
@ -329,9 +336,20 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector <Plugi
TWEAK_HOOK("nestbox-color", nestbox_color_hook, drawBuilding);
TWEAK_HOOK("partial-items", partial_items_hook_bar, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_drink, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_glob, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_liquid_misc, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_powder_misc, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_cloth, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_sheet, getItemDescription);
TWEAK_HOOK("partial-items", partial_items_hook_thread, getItemDescription);
TWEAK_HOOK("pausing-fps-counter", dwarfmode_pausing_fps_counter_hook, render);
TWEAK_HOOK("pausing-fps-counter", title_pausing_fps_counter_hook, render);
TWEAK_HOOK("reaction-gloves", reaction_gloves_hook, produce);
TWEAK_HOOK("shift-8-scroll", shift_8_scroll_hook, feed);
TWEAK_HOOK("stable-cursor", stable_cursor_hook, feed);
@ -344,8 +362,6 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector <Plugi
TWEAK_HOOK("tradereq-pet-gender", pet_gender_hook, render);
TWEAK_HOOK("reaction-gloves", reaction_gloves_hook, produce);
return CR_OK;
}

@ -0,0 +1,23 @@
// When displaying the names of partially-consumed items, show the percentage remaining
// Potentially useful for revealing why random pieces of cloth or thread aren't suitable for jobs
#define DEFINE_PARTIAL_ITEM_TWEAK(TYPE, DIM) \
struct partial_items_hook_##TYPE : df::item_##TYPE##st { \
typedef df::item_##TYPE##st interpose_base; \
DEFINE_VMETHOD_INTERPOSE(void, getItemDescription, (std::string *str, int8_t plurality)) \
{ \
INTERPOSE_NEXT(getItemDescription)(str, plurality); \
if (dimension != DIM) \
str->append(stl_sprintf(" (%i%%)", std::max(1, dimension * 100 / DIM))); \
} \
}; \
IMPLEMENT_VMETHOD_INTERPOSE(partial_items_hook_##TYPE, getItemDescription);
DEFINE_PARTIAL_ITEM_TWEAK(bar, 150)
DEFINE_PARTIAL_ITEM_TWEAK(drink, 150)
DEFINE_PARTIAL_ITEM_TWEAK(glob, 150)
DEFINE_PARTIAL_ITEM_TWEAK(liquid_misc, 150)
DEFINE_PARTIAL_ITEM_TWEAK(powder_misc, 150)
DEFINE_PARTIAL_ITEM_TWEAK(cloth, 10000)
DEFINE_PARTIAL_ITEM_TWEAK(sheet, 10000)
DEFINE_PARTIAL_ITEM_TWEAK(thread, 15000)

@ -1 +1 @@
Subproject commit d9e390cd5509458bfbc0c1e2ecb01ae96bddc015
Subproject commit 74f03c0e4a5a7818b7a1cbc3576ce2c3d30d3696

@ -67,6 +67,8 @@ function test.wrap()
expect.eq('hel\nlo\nwor\nld', ('hello world'):wrap(3))
expect.eq('hel\nloo\nwor\nldo', ('helloo worldo'):wrap(3))
expect.eq('', (''):wrap())
expect.error_match('expected width > 0', function() ('somestr'):wrap(0) end)
end
function test.escape_pattern()

@ -0,0 +1,76 @@
local rng = require('plugins.cxxrandom')
function test.cxxrandom_distributions()
rng.normal_distribution:new(0,5)
rng.real_distribution:new(-1,1)
rng.int_distribution:new(-20,20)
rng.bool_distribution:new(0.00000000001)
rng.num_sequence:new(-1000,1000)
-- no errors, no problem
end
--[[
The below tests pass with their given seeds, if they begin failing
for a given platform, or all around, new seeds should be found.
Note: these tests which assert RNG, are mere sanity checks
to ensure things haven't been severely broken by any changes
]]
function test.cxxrandom_seed()
local nd = rng.normal_distribution:new(0,500000)
local e1 = rng.MakeNewEngine(1)
local e2 = rng.MakeNewEngine(1)
local e3 = rng.MakeNewEngine(2)
local g1 = rng.crng:new(e1, true, nd)
local g2 = rng.crng:new(e2, true, nd)
local g3 = rng.crng:new(e3, true, nd)
local v1 = g1:next()
expect.eq(v1, g2:next())
expect.ne(v1, g3:next())
end
function test.cxxrandom_ranges()
local e1 = rng.MakeNewEngine(1)
local g1 = rng.crng:new(e1, true, rng.normal_distribution:new(0,1))
local g2 = rng.crng:new(e1, true, rng.real_distribution:new(-5,5))
local g3 = rng.crng:new(e1, true, rng.int_distribution:new(-5,5))
local g4 = rng.crng:new(e1, true, rng.num_sequence:new(-5,5))
for i = 1, 10 do
local a = g1:next()
local b = g2:next()
local c = g3:next()
local d = g4:next()
expect.ge(a, -5)
expect.ge(b, -5)
expect.ge(c, -5)
expect.ge(d, -5)
expect.le(a, 5)
expect.le(b, 5)
expect.le(c, 5)
expect.le(d, 5)
end
local gb = rng.crng:new(e1, true, rng.bool_distribution:new(0.00000000001))
for i = 1, 10 do
expect.false_(gb:next())
end
end
function test.cxxrandom_exports()
local id = rng.GenerateEngine(0)
rng.NewSeed(id, 2022)
expect.ge(rng.rollInt(id, 0, 1000), 0)
expect.ge(rng.rollDouble(id, 0, 1), 0)
expect.ge(rng.rollNormal(id, 5, 1), 0)
expect.true_(rng.rollBool(id, 0.9999999999))
local sid = rng.MakeNumSequence(0,8)
rng.AddToSequence(sid, 9)
rng.ShuffleSequence(sid, id)
for i = 1, 10 do
local v = rng.NextInSequence(sid)
expect.ge(v, 0)
expect.le(v, 9)
end
rng.DestroyNumSequence(sid)
rng.DestroyEngine(id)
end