diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4af9f5653..e8506ae9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/.gitignore b/.gitignore index 3867b9dbf..8e401a7df 100644 --- a/.gitignore +++ b/.gitignore @@ -72,5 +72,4 @@ tags .idea # external plugins -/plugins/external/ /plugins/CMakeLists.custom.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47f314e2a..26d135ca2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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$)' diff --git a/data/examples/init/onMapLoad_dreamfort.init b/data/examples/init/onMapLoad_dreamfort.init index 8e9c0134d..c75620846 100644 --- a/data/examples/init/onMapLoad_dreamfort.init +++ b/data/examples/init/onMapLoad_dreamfort.init @@ -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 diff --git a/dfhack.init-example b/dfhack.init-example index 702ce5276..d181fcaa7 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -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. diff --git a/docs/Authors.rst b/docs/Authors.rst index a6fa5a3d8..38fe19862 100644 --- a/docs/Authors.rst +++ b/docs/Authors.rst @@ -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 diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 11c1d41e3..caaab0040 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -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 ======= diff --git a/docs/Plugins.rst b/docs/Plugins.rst index bad1629f7..3b42bc16a 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -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:: diff --git a/docs/Remote.rst b/docs/Remote.rst index c41a14058..de741f66a 100644 --- a/docs/Remote.rst +++ b/docs/Remote.rst @@ -75,10 +75,11 @@ from other (non-C++) languages, including: - `RemoteClientDF-Net `_ for C# - `dfhackrpc `_ for Go -- `dfhack-remote `_ for JavaScript +- `dfhack-remote `__ for JavaScript - `dfhack-client-qt `_ for C++ with Qt - `dfhack-client-python `_ for Python (adapted from :forums:`"Blendwarf" <178089>`) - `dfhack-client-java `_ for Java +- `dfhack-remote `__ for Rust Protocol description diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 026558ab0..4ec165308 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -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 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 diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 26e20f748..e476ffa11 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -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 diff --git a/library/lua/gui.lua b/library/lua/gui.lua index a4541a6d8..8521e1dfe 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -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 diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 687c7176e..51f346bbd 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -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 diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index b79f7a9b7..e56211233 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -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" diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 9306a75fa..b8782bd04 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -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) diff --git a/library/xml b/library/xml index b8d48430a..5fe90d06e 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit b8d48430aa13570a668464ce40294a589f1f0b1f +Subproject commit 5fe90d06ebad4f9686545b7c961c2c07b8bcb799 diff --git a/onLoad.init-example b/onLoad.init-example deleted file mode 100644 index 1d32f07c9..000000000 --- a/onLoad.init-example +++ /dev/null @@ -1 +0,0 @@ -repeat -name warn-starving -time 10 -timeUnits days -command [ warn-starving ] diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 22983d617..b8cd25860 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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") diff --git a/plugins/cxxrandom.cpp b/plugins/cxxrandom.cpp index 159edaefc..12f043214 100644 --- a/plugins/cxxrandom.cpp +++ b/plugins/cxxrandom.cpp @@ -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 &commands) -{ +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &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 m_engines; - uint16_t counter = 0; + EnginesKeeper() = default; + std::unordered_map 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 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 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 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 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 m_sequences; - uint16_t counter = 0; + SequenceKeeper() = default; + std::unordered_map 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); } diff --git a/plugins/external/.gitignore b/plugins/external/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/plugins/external/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/plugins/lua/cxxrandom.lua b/plugins/lua/cxxrandom.lua index 78e363bef..542575b9c 100644 --- a/plugins/lua/cxxrandom.lua +++ b/plugins/lua/cxxrandom.lua @@ -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 diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 1ea958f5b..38ece6970 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -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 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) diff --git a/scripts b/scripts index d9e390cd5..74f03c0e4 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d9e390cd5509458bfbc0c1e2ecb01ae96bddc015 +Subproject commit 74f03c0e4a5a7818b7a1cbc3576ce2c3d30d3696 diff --git a/test/library/string.lua b/test/library/string.lua index d45be4b3f..d22f262bf 100644 --- a/test/library/string.lua +++ b/test/library/string.lua @@ -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() diff --git a/test/plugins/cxxrandom.lua b/test/plugins/cxxrandom.lua new file mode 100644 index 000000000..6b11e1937 --- /dev/null +++ b/test/plugins/cxxrandom.lua @@ -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