diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 4c04ac72d8..5eda3d6e15 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -371,3 +371,9 @@ workorder-recheck ================= Tool to set 'Checking' status of the selected work order, allowing conditions to be reevaluated. Merged into `orders`. + +.. _nestboxes: + +nestboxes +================= +Migrated to lua and merged into autobutcher plugin diff --git a/docs/changelog.txt b/docs/changelog.txt index ce6a297b5e..6858c48e43 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,6 +65,7 @@ Template for new versions: - Quickfort blueprint library: ``aquifer_tap`` blueprint walkthough rewritten for clarity - Quickfort blueprint library: ``aquifer_tap`` blueprint now designated at priority 3 and marks the stairway tile below the tap in "blueprint" mode to prevent drips while the drainage pipe is being prepared - `preserve-rooms`: automatically release room reservations for captured squad members. we were kidding ourselves with our optimistic kept reservations. they're unlikely to come back : (( +- `autobutcher`: added nestboxes feature giving more control how fertile eggs should be protected, with possibility to adjust configuration per race ## Documentation @@ -77,6 +78,7 @@ Template for new versions: - ``dfhack.units``: new function ``setPathGoal`` ## Removed +- `nestboxes`: merged into `autobutcher` # 50.14-r1 diff --git a/docs/plugins/autobutcher.rst b/docs/plugins/autobutcher.rst index b6133fbf21..b050c108ef 100644 --- a/docs/plugins/autobutcher.rst +++ b/docs/plugins/autobutcher.rst @@ -30,6 +30,21 @@ The default targets are: 2 male kids, 4 female kids, 2 male adults, and 4 female adults. Note that you may need to set a target above 1 to have a reliable breeding population due to asexuality etc. +Nestboxes module extends autobutcher functionality to monitoring and protection +of fertile eggs. It facilitates steady supplay of eggs protected for breading +purposes while maintaining egg based food production. With default settings it +will forbid fertile eggs in claimed nestboxes up to 4 + number of annimals +missing to autobutcher target. This will allow for larger number of hatchilings +in initial breadin program phase when increasing livestock. +When population reaches intended target only base amount of eggs will be left +for breading purpose. +It is possible to alter this behaviour and compleatly stop protection of eggs +when population target is reached. +In case of clutch larger than needed target eggs will be split in two separate +stacks and only one of them forbidden. +Check for forbidding eggs is made when fertile eggs are laid for one of +watched races. + Usage ----- @@ -78,6 +93,50 @@ Usage ``autobutcher list_export`` Print commands required to set the current settings in another fort. +``autobutcher nestboxes enable (autobutcher nb e)`` +``autobutcher nestboxes disable (autobutcher nb d)`` + + It is possible to enable/ disable autobutcher nestboxes module. + +``autobutcher nestboxes ignore (autobutcher nb i <1/0>)`` + + By default nestboxes module respects main plugin's enabled status, + autowatch and watched status for specific races. + It is possible to allow nestboxes module to ignore that and work + on it's own. In case like that missing animal count will not be added + to target of protected eggs. + +``autobutcher nestboxes target `` +``autobutcher nb t `` + + Nestboxes target command allows to change how script handles specific + animal race, DEFAULT settings for new races or ALL curently watched races + and default value for new ones. + parameter accepts "DEFAULT", "ALL", creature id (e.g. BIRD_TURKEY) + or race id (190 for Turkey) + base number of fertile eggs that will be protected by + frobidding them in nestboxes. Default 4. + true/false value, if set to true animal count missing to autobutcher + popualtion targets will be added to base target for protected eggs. + Default true. + if set to true module will stop protection of eggs for race as long + as population target is maintained. Default true. + If eggs laid by race should be monitored and protected. + Default true. + If parameter is not specified already existing value will be mantained. + If new race is added missing values will be taken from default settings. + +``autobutcher nestboxes split_stacks (autobutcher nb s <1/0>)`` + + split_stacks command allows to specify how egg stacks that are only + partialy over target should be handled. If set to false whole stack will + be forbidden. If set to true only eggs below target will be forbidden, + remaining part of stack will be separated and left for dwarves to collect. + +``autobutcher nestboxes clear (autobutcher nb clear)`` + + Remove all settings for module and restore them to initial default values. + To see a list of all races, run this command:: devel/query --table df.global.world.raws.creatures.all --search ^creature_id --maxdepth 1 @@ -116,3 +175,27 @@ fortress:: autobutcher target 2 2 4 2 ALPACA SHEEP LLAMA autobutcher target 5 5 6 2 PIG autobutcher target 0 0 0 0 new + + + autobutcher nb t BEAK_DOG 10 1 1 1 + autobutcher nestboxes target BEAK_DOG 5 true true true + +Command sets base target for beak dog eggs to 5, animals missing to population tresholds will be added to base target. +Once autobutcher population target is reached no new eggs will be forbidden as long as population is at or above target. + + autobutcher nb t DEFAULT 4 1 0 0 + autobutcher nestboxes target DEFAULT 4 true true false + +Command will change default settings for watching new races disabling it. + + autobutcher nb t ALL 15 0 1 1 + autobutcher nestboxes target ALL 15 false true true + +Command will change setting for all currently watched egg races as well as default ones. +Target for protected eggs is set to 15, missing animals count to livestock targets is not taken into account. +Once population target is reached eggs will no longer be protected. All current and new races will be watched. + + autobutcher nestboxes split_stacks false + autobutcher nb s 0 + +Disable spliting of egg stacks. diff --git a/docs/plugins/nestboxes.rst b/docs/plugins/nestboxes.rst deleted file mode 100644 index 3c07cc30ca..0000000000 --- a/docs/plugins/nestboxes.rst +++ /dev/null @@ -1,18 +0,0 @@ -nestboxes -========= - -.. dfhack-tool:: - :summary: Protect fertile eggs incubating in a nestbox. - :tags: fort auto animals - :no-command: - -This plugin will automatically scan for and forbid fertile eggs incubating in a -nestbox so that dwarves won't come to collect them for eating. The eggs will -hatch normally, even when forbidden. - -Usage ------ - -:: - - enable nestboxes diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index ddea7702f3..6021802e53 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -4169,6 +4169,27 @@ static int internal_getClipboardTextCp437Multiline(lua_State *L) { return 1; } +static int internal_get_persistent_data_int(lua_State* L, get_data_fn get_data) { + CoreSuspender suspend; + + PersistentDataItem data = get_data(L); + + if (!data.isValid() || !lua_isnumber(L, 2)){ + lua_pushnil(L); + } + else { + const int idx = lua_tointeger(L, 2); + lua_pushinteger(L, data.get_int(idx)); + } + + return 1; +} + +static int internal_readPersistentSiteDataInt(lua_State* L) { + return internal_get_persistent_data_int(L, get_site_data); +} + + static const luaL_Reg dfhack_internal_funcs[] = { { "getPE", internal_getPE }, { "getMD5", internal_getmd5 }, @@ -4204,6 +4225,7 @@ static const luaL_Reg dfhack_internal_funcs[] = { { "getPerfCounters", internal_getPerfCounters }, { "getPreferredNumberFormat", internal_getPreferredNumberFormat }, { "getClipboardTextCp437Multiline", internal_getClipboardTextCp437Multiline }, + { "readPersistentSiteConfigInt", internal_readPersistentSiteDataInt }, { NULL, NULL } }; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f0b35ea9ef..999e248323 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -136,7 +136,6 @@ if(BUILD_SUPPORTED) #dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua) dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua) #dfhack_plugin(mode mode.cpp) - dfhack_plugin(nestboxes nestboxes.cpp) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index aa92709ada..0372b5a8c6 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -971,6 +971,42 @@ static int autobutcher_getWatchList(lua_State *L) { return 1; } +static int getMax(int first, int second) { + return first > second ? first : second; +} +// push info used by nestboxes +static int autobutcher_getInfoForNestboxes(lua_State* L) { + PersistentDataItem rconfig; + color_ostream* out = Lua::GetOutput(L); + + if (lua_isnumber(L, 1)) { + int raceId = lua_tointeger(L, 1); + lua_newtable(L); + int ctable = lua_gettop(L); + Lua::SetField(L, config.get_bool(CONFIG_IS_ENABLED), ctable, "enabled"); + + if (!out) + out = &Core::getInstance().getConsole(); + + WatchedRace* w; + if (watched_races.count(raceId)) { + w = watched_races[raceId]; + WatchedRace* tally = checkRaceStocksTotal(*out, raceId); + Lua::SetField(L, w->isWatched, ctable, "watched"); + // missing animals count for race, + // diff between target for child/adult female/male and current amounts, + // ignore amounts over target + int mac = getMax(w->fk - tally->fk_units.size(), 0); + mac += getMax(w->mk - tally->mk_units.size(), 0); + mac += getMax(w->fa - tally->fa_units.size(), 0); + mac += getMax(w->ma - tally->ma_units.size(), 0); + Lua::SetField(L, mac, ctable, "mac"); + delete tally; + } + } + return 1; +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(autowatch_isEnabled), DFHACK_LUA_FUNCTION(autowatch_setEnabled), @@ -986,5 +1022,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(autobutcher_getSettings), DFHACK_LUA_COMMAND(autobutcher_getWatchList), + DFHACK_LUA_COMMAND(autobutcher_getInfoForNestboxes), DFHACK_LUA_END }; diff --git a/plugins/lua/autobutcher.lua b/plugins/lua/autobutcher.lua index 146f8f22cd..47d3059193 100644 --- a/plugins/lua/autobutcher.lua +++ b/plugins/lua/autobutcher.lua @@ -1,6 +1,7 @@ local _ENV = mkmodule('plugins.autobutcher') local argparse = require('argparse') +local nestboxes = require('plugins.autobutcher.nestboxes') local function is_int(val) return val and val == math.floor(val) @@ -45,6 +46,13 @@ local function process_races(opts, races, start_idx) end end +local function reload_modules() + reload('plugins.autobutcher.common') + reload('plugins.autobutcher.nestboxesEvent') + reload('plugins.autobutcher.nestboxes') + reload('plugins.autobutcher') +end + function parse_commandline(opts, args) local positionals = process_args(opts, args) @@ -65,6 +73,10 @@ function parse_commandline(opts, args) opts.fa = check_nonnegative_int(positionals[4]) opts.ma = check_nonnegative_int(positionals[5]) process_races(opts, positionals, 6) + elseif command == 'reload' then + reload_modules() + elseif string.upper(command) == 'NESTBOXES' or string.upper(command) =='NB' then + nestboxes.handleCommand(positionals, opts) else qerror(('unrecognized command: "%s"'):format(command)) end diff --git a/plugins/lua/autobutcher/common.lua b/plugins/lua/autobutcher/common.lua new file mode 100644 index 0000000000..5adebfc95f --- /dev/null +++ b/plugins/lua/autobutcher/common.lua @@ -0,0 +1,34 @@ +local _ENV = mkmodule('plugins.autobutcher.common') + +verbose = verbose or nil +prefix = prefix or '' + +function printLocal(text) + print(prefix .. ': ' .. text) +end + +function handleError(text) + qerror(prefix .. ': ' .. text) +end + +function printDetails(text) + if verbose then + printLocal(text) + end +end + +function dumpToString(o) + if type(o) == 'table' then + local s = '{ ' + for k, v in pairs(o) do + if type(k) ~= 'number' then + k = '"' .. k .. '"' + end + s = s .. '[' .. k .. '] = ' .. dumpToString(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end +end +return _ENV diff --git a/plugins/lua/autobutcher/nestboxes.lua b/plugins/lua/autobutcher/nestboxes.lua new file mode 100644 index 0000000000..215a8b3945 --- /dev/null +++ b/plugins/lua/autobutcher/nestboxes.lua @@ -0,0 +1,357 @@ +local _ENV = mkmodule('plugins.autobutcher.nestboxes') +--------------------------------------------------------------------------------------------------- +local nestboxesCommon = require('plugins.autobutcher.common') +local nestboxesEvent = require('plugins.autobutcher.nestboxesEvent') +local eventful = require('plugins.eventful') +local utils = require('utils') +local printLocal = nestboxesCommon.printLocal +local printDetails = nestboxesCommon.printDetails +local handleError = nestboxesCommon.handleError +local dumpToString = nestboxesCommon.dumpToString +local GLOBAL_KEY = 'autobutcher.nestboxes' +local EVENT_FREQ = 7 +local string_or_int_to_boolean = { + ['true'] = true, + ['false'] = false, + ['1'] = true, + ['0'] = false, + ['Y'] = true, + ['N'] = false, + [1] = true, + [0] = false +} +local function getBoolean(value) + return string_or_int_to_boolean[value] +end +--------------------------------------------------------------------------------------------------- +local default_table = { + watched = true, -- monitor eggs for race + target = 4, --basic target for protected(forbidden) eggs + ama = true, --Add Missing Animals to basic target for prottected eggs, difference between current amount of animals and target set for autobutcher, speeds up population growth in initial phase whiile limitting population explosion near end if egg target has low value + stop = true -- stop protecting eggs once autobutcher target for live animals is reached +} +--------------------------------------------------------------------------------------------------- +local function getDefaultState() + return { + enabled = false, -- enabled status for nestboxes + verbose = false, -- verbose mode + ignore_autobutcher = false, -- if set to true nestboxes will work on their own ignoring autobutcher enabled status or if it's watching race + migration_from_cpp_to_lua_done = false, --flag to handle migration from cpp nestboxes + split_stacks = true, -- should eggs stacks be split if only part is over limit, if set to false whole stack will be forbiden + default = default_table, -- default settings applied to new races + config_per_race = {} -- config per race + } +end --getDefaultState +--------------------------------------------------------------------------------------------------- +state = state or getDefaultState() +--------------------------------------------------------------------------------------------------- +local function persistState() + printDetails(('start persistState')) + local state_to_persist = {} + state_to_persist = utils.clone(state) + state_to_persist.config_per_race = {} + + if state.config_per_race ~= nil then + for k, v in pairs(state.config_per_race) do + state_to_persist.config_per_race[tostring(k)] = v + end + end + + dfhack.persistent.saveSiteData(GLOBAL_KEY, state_to_persist) + printDetails(('end persistState')) +end --persistState +--------------------------------------------------------------------------------------------------- +local function readPersistentConfig(key, index) + if dfhack.internal.readPersistentSiteConfigInt ~= nil then + return dfhack.internal.readPersistentSiteConfigInt(key, index) + end + return nil +end -- readPersistentConfig + +--------------------------------------------------------------------------------------------------- +local function migrateEnabledStatusFromCppNestboxes() + printLocal('About to attempt migration from nestboxes') + local nestboxes_status = readPersistentConfig('nestboxes/config', '0') + printLocal( + ('Migrating status %s from cpp nestboxes'):format(getBoolean(nestboxes_status) and 'enabled' or 'disabled') + ) + state.enabled = getBoolean(nestboxes_status) or false + state.migration_from_cpp_to_lua_done = true + dfhack.persistent.deleteSiteData('nestboxes/config') + persistState() + printLocal('Migration from cpp to lua done') +end --migrateEnabledStatusFromCppNestboxes +--------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------- +local function initEggwatchCommon() + nestboxesCommon.verbose = state.verbose + nestboxesCommon.prefix = GLOBAL_KEY +end --initEggwatchCommon +--------------------------------------------------------------------------------------------------- +--- Load the saved state of the script +local function loadState() + printDetails(('start loadState')) + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, getDefaultState()) + local processed_persisted_data = {} + if persisted_data ~= nil then + processed_persisted_data = utils.clone(persisted_data) + processed_persisted_data.config_per_race = {} + if persisted_data.config_per_race ~= nil then + for k, v in pairs(persisted_data.config_per_race) do + local default = utils.clone(default_table) + processed_persisted_data.config_per_race[tonumber(k)] = utils.assign(default, v) + end + end + end + state = getDefaultState() + utils.assign(state, processed_persisted_data) + if not state.migration_from_cpp_to_lua_done then + migrateEnabledStatusFromCppNestboxes() + end + initEggwatchCommon() + printDetails(('end loadState')) +end --loadState +--------------------------------------------------------------------------------------------------- +local function updateEventListener() + printDetails(('start updateEventListener')) + if state.enabled then + eventful.enableEvent(eventful.eventType.ITEM_CREATED, EVENT_FREQ) + eventful.onItemCreated[GLOBAL_KEY] = checkItemCreated + printLocal(('Subscribing in eventful for %s with frequency %s'):format('ITEM_CREATED', EVENT_FREQ)) + else + eventful.onItemCreated[GLOBAL_KEY] = nil + printLocal(('Unregistering from eventful for %s'):format('ITEM_CREATED')) + end + printDetails(('end updateEventListener')) +end --updateEventListener +--------------------------------------------------------------------------------------------------- +local function doEnable() + printDetails(('start doEnable')) + state.enabled = true + updateEventListener() + printDetails(('end doEnable')) +end --doEnable +--------------------------------------------------------------------------------------------------- +local function doDisable() + printDetails(('start doDisable')) + state.enabled = false + updateEventListener() + printDetails(('end doDisable')) +end --doDisable +--------------------------------------------------------------------------------------------------- +local function getConfigForRace(race) + printDetails(('start getConfigForRace')) + printDetails(('getting config for race %s '):format(race)) + if state.config_per_race[race] == nil and state.default.watched then + state.config_per_race[race] = state.default + persistState() + end + printDetails(('end getConfigForRace')) + return state.config_per_race[race] +end --getConfigForRace +--------------------------------------------------------------------------------------------------- +local function validateCreatureOrRace(value) + printDetails(('start validate_creature_id')) + if value == 'DEFAULT' or value == 'ALL' then + return value + end + for id, c in ipairs(df.global.world.raws.creatures.all) do + if c.creature_id == value or id == value then + for _, c in ipairs(c.caste) do + if c.flags.LAYS_EGGS then + return id + end + end + handleError(('%s is not egglayer'):format(value)) + end + end + handleError(('could not find %s'):format(value)) +end --validateCreatureOrRace +--------------------------------------------------------------------------------------------------- +local function setTarget(target_race, target_count, add_missing_animals, stop, watch_race) + printDetails(('start setTarget')) + + if type(target_race) == 'string' then + target_race = string.upper(target_race) + end + + if target_race == nil or target_race == '' then + handleError('must specify DEFAULT, ALL, valid creature_id or race id') + end + + local new_config = { + watched = getBoolean(watch_race), + target = tonumber(target_count), + ama = getBoolean(add_missing_animals), + stop = getBoolean(stop) + } + + local race = validateCreatureOrRace(target_race) + if race == 'DEFAULT' then + utils.assign(state.default, new_config) + elseif race == 'ALL' then + for _, v in pairs(state.config_per_race) do + utils.assign(v, new_config) + utils.assign(state.default, new_config) + end + elseif race >= 0 then + utils.assign(state.config_per_race[race], new_config) + else + handleError('must specify DEFAULT, ALL, valid creature_id or race id') + end + printDetails(('end setTarget')) +end --setTarget +--------------------------------------------------------------------------------------------------- +local function setSplitStacks(value) + state.split_stacks = getBoolean(value) +end +--------------------------------------------------------------------------------------------------- +local function clearConfig() + state = getDefaultState() + updateEventListener() +end +--------------------------------------------------------------------------------------------------- +local function setIgnore(value) + state.ignore_autobutcher = getBoolean(value) +end +--------------------------------------------------------------------------------------------------- +local function setVerbose(value) + state.verbose = getBoolean(value) + nestboxesCommon.verbose = state.verbose +end +--------------------------------------------------------------------------------------------------- +local function format_target_count_row(category, row) + return (('%s: watched: %s; target: %s; ama: %s; stop: %s'):format( + category, + row.watched and 'enabled' or 'disabled', + tostring(row.target), + row.ama and 'enabled' or 'disabled', + row.stop and 'enabled' or 'disabled' + )) +end +--------------------------------------------------------------------------------------------------- +local function printStatus() + printLocal(('Status %s.'):format(state.enabled and 'enabled' or 'disabled')) + printLocal(('Stack splitting: %s'):format(state.split_stacks and 'enabled' or 'disabled')) + printLocal( + ('%s autobutcher\'s enabled status'):format(state.ignore_autobutcher and 'Ignoring' or 'Tespecting') + ) + printDetails(('verbose mode is %s'):format(state.verbose and 'enabled' or 'disabled')) + printDetails( + ('Migration from cpp to lua is %s'):format(state.migration_from_cpp_to_lua_done and 'done' or 'not done') + ) + printLocal(format_target_count_row('Default', state.default)) + if state.config_per_race ~= nil then + for k, v in pairs(state.config_per_race) do + printLocal(format_target_count_row(df.global.world.raws.creatures.all[k].creature_id, v)) + end + end + --printDetails(dumpToString(state)) +end +--------------------------------------------------------------------------------------------------- +local function handleOnStateChange(sc) + if sc == SC_MAP_UNLOADED then + doDisable() + return + end + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + loadState() + printStatus() + updateEventListener() +end --handleOnStateChange +--------------------------------------------------------------------------------------------------- +local function getInfoFromAutobutcher(race) + local v_return = { + enabled = false, + watched = false, + mac = 0 + } + printDetails(('getInfoFromAutobutcher for race= %s'):format(race)) + local autobutcher = require('plugins.autobutcher') + if autobutcher.autobutcher_getInfoForNestboxes ~= nil then + local autobutcher_return = autobutcher.autobutcher_getInfoForNestboxes(race) + if autobutcher_return ~= nil then + utils.assign(v_return, autobutcher_return) + else + printDetails(('got nil from autobutcher_return')) + end + else + printDetails(('got nil from autobutcher_getInfoForNestboxes')) + end + return v_return +end --getInfoFromAutobutcher +--------------------------------------------------------------------------------------------------- +-- +--------------------------------------------------------------------------------------------------- +-- checkItemCreated function, called from eventfful on ITEM_CREATED event +function checkItemCreated(item_id) + local item = df.item.find(item_id) + if item == nil or df.item_type.EGG ~= item:getType() or not nestboxesEvent.validateEggs(item) then + return + end + + local autobutcher_info = getInfoFromAutobutcher(item.race) + if not ((autobutcher_info.watched and autobutcher_info.enabled) or state.ignore_autobutcher) then + printDetails(('Did not check eggs, race %s not watched by autobutcher, autobutcher settings respected'):format(item.race)) + return + end + + local race_config = getConfigForRace(item.race) + if race_config == nil then + printDetails(('Did not check eggs, race %s without config and no new races are being watched'):format(item.race)) + return + end + + if (race_config.stop and autobutcher_info.mac == 0 and not state.ignore_autobutcher) then + printDetails(('Did not check eggs, missing animal count for race %s is 0 and stop once animal target reached is enabled'):format(item.race)) + return + end + + if (race_config.watched) then + nestboxesEvent.handleEggs(item, race_config.target, race_config.ama, state.split_stacks, autobutcher_info.mac) + else + printDetails(('Did not check eggs, race %s not watched by %s'):format(item.race, GLOBAL_KEY)) + end +end --checkItemCreated +--------------------------------------------------------------------------------------------------- +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + handleOnStateChange(sc) +end +--------------------------------------------------------------------------------------------------- +function handleCommand(positionals, opts) + loadState() + local command = positionals[2] + + if command ~= nil then + command = string.upper(command) + end + + if command == 'ENABLE' or command == 'E' then + doEnable() + elseif command == 'DISABLE' or command == 'D' then + doDisable() + elseif command == 'UPDATE' or command == 'U' then + updateEventListener() + elseif command == 'TARGET' or command == 'T' then + setTarget(positionals[3], positionals[4], positionals[5], positionals[6], positionals[7]) + elseif command == 'IGNORE' or command == 'I' then + setIgnore(positionals[3]) + elseif command == 'VERBOSE' or command == 'V' then + setVerbose(positionals[3]) + elseif command == 'SPLIT_STACKS' or command == 'S' then + setSplitStacks(positionals[3]) + elseif command == 'CLEAR' then + clearConfig() + elseif positionals[2] ~= nil then + handleError(('Command "% s" is not recognized'):format(positionals[2])) + end + + printStatus() + persistState() +end --handleCommand +--------------------------------------------------------------------------------------------------- +-- +return _ENV diff --git a/plugins/lua/autobutcher/nestboxesEvent.lua b/plugins/lua/autobutcher/nestboxesEvent.lua new file mode 100644 index 0000000000..ca353a801c --- /dev/null +++ b/plugins/lua/autobutcher/nestboxesEvent.lua @@ -0,0 +1,225 @@ +local _ENV = mkmodule('plugins.autobutcher.nestboxesEvent') +--------------------------------------------------------------------------------------------------- +local nestboxesCommon = require('plugins.autobutcher.common') +local printLocal = nestboxesCommon.printLocal +local printDetails = nestboxesCommon.printDetails +local utils = require('utils') +--------------------------------------------------------------------------------------------------- +local function copyEggFields(source_egg, target_egg) + printDetails('start copyEggFields') + target_egg.incubation_counter = source_egg.incubation_counter + target_egg.egg_flags = utils.clone(source_egg.egg_flags) + target_egg.hatchling_flags1 = utils.clone(source_egg.hatchling_flags1) + target_egg.hatchling_flags2 = utils.clone(source_egg.hatchling_flags2) + target_egg.hatchling_flags3 = utils.clone(source_egg.hatchling_flags3) + target_egg.hatchling_flags4 = utils.clone(source_egg.hatchling_flags4) + target_egg.hatchling_training_level = source_egg.hatchling_training_level + target_egg.hatchling_mother_id = source_egg.hatchling_mother_id + target_egg.mother_hf = source_egg.mother_hf + target_egg.father_hf = source_egg.mother_hf + target_egg.mothers_caste = source_egg.mothers_caste + target_egg.fathers_caste = source_egg.fathers_caste + + local mothers_genes = df.unit_genes:new() + mothers_genes.appearance:assign(source_egg.mothers_genes.appearance) + mothers_genes.colors:assign(source_egg.mothers_genes.colors) + + local fathers_genes = df.unit_genes:new() + fathers_genes.appearance:assign(source_egg.fathers_genes.appearance) + fathers_genes.colors:assign(source_egg.fathers_genes.colors) + + target_egg.mothers_genes = mothers_genes + target_egg.fathers_genes = fathers_genes + printDetails("mothers_genes fathers_genes done") + + target_egg.hatchling_civ_id = source_egg.hatchling_civ_id + printDetails('hatchling_civ_id done') + printDetails('end copyEggFields') +end --copyEggFields +--------------------------------------------------------------------------------------------------- +local function resizeEggStack(egg_stack, new_stack_size) + printDetails('start resizeEggStack') + egg_stack.stack_size = new_stack_size + --TODO check if weight or size need adjustment + printDetails('end resizeEggStack') +end --resizeEggStack +--------------------------------------------------------------------------------------------------- +local function createNewEggStack(original_eggs, new_stack_count) + printDetails('start createNewEggStack') + printDetails('about to create new egg stack') + printDetails(('type= %s'):format(original_eggs:getType())) + printDetails(('creature= %s'):format(original_eggs.race)) + printDetails(('caste= %s '):format(original_eggs.caste)) + printDetails(('stack size for new eggs = %s '):format(new_stack_count)) + + local created_items = + dfhack.items.createItem( + df.unit.find(original_eggs.hatchling_mother_id), + original_eggs:getType(), + -1, + original_eggs.race, + original_eggs.caste + ) + printDetails('created new egg stack') + local created_egg_stack = created_items[0] or created_items[1] + printDetails(df.creature_raw.find(created_egg_stack.race).creature_id) + printDetails('about to copy fields from orginal eggs') + copyEggFields(original_eggs, created_egg_stack) + + printDetails('about to resize new egg stack') + resizeEggStack(created_egg_stack, new_stack_count) + + printDetails('about to move new stack to nestbox') + if dfhack.items.moveToBuilding(created_egg_stack, dfhack.items.getHolderBuilding(original_eggs)) then + printDetails('moved new egg stack to nestbox') + else + printLocal('move of separated eggs to nestbox failed') + end + printDetails('end createNewEggStack') +end --createNewEggStack +--------------------------------------------------------------------------------------------------- +local function splitEggStack(source_egg_stack, to_be_left_in_source_stack) + printDetails('start splitEggStack') + local egg_count_in_new_stack_size = source_egg_stack.stack_size - to_be_left_in_source_stack + if egg_count_in_new_stack_size > 0 then + createNewEggStack(source_egg_stack, egg_count_in_new_stack_size) + resizeEggStack(source_egg_stack, to_be_left_in_source_stack) + else + printDetails('nothing to do, wrong egg_count_in_new_stack_size') + end + printDetails('end splitEggStack') +end --splitEggStack +--------------------------------------------------------------------------------------------------- +local function countForbiddenEggsForRaceInClaimedNestobxes(race) + printDetails(('start countForbiddenEggsForRaceInClaimedNestobxes')) + local eggs_count = 0 + for _, nestbox in ipairs(df.global.world.buildings.other.NEST_BOX) do + if nestbox.claimed_by ~= -1 then + printDetails(('Found claimed nextbox')) + for _, nestbox_contained_item in ipairs(nestbox.contained_items) do + if nestbox_contained_item.use_mode == df.building_item_role_type.TEMP then + printDetails(('Found claimed nextbox containing items')) + if df.item_type.EGG == nestbox_contained_item.item:getType() then + printDetails(('Found claimed nextbox containing items that are eggs')) + if nestbox_contained_item.item.egg_flags.fertile and nestbox_contained_item.item.flags.forbid then + printDetails(('Eggs are fertile and forbidden')) + if nestbox_contained_item.item.race == race then + printDetails(('Eggs belong to %s'):format(race)) + printDetails( + ('eggs_count %s + new %s'):format( + eggs_count, + nestbox_contained_item.item.stack_size) + ) + eggs_count = eggs_count + nestbox_contained_item.item.stack_size + printDetails(('eggs count after adding current nestbox %s '):format(eggs_count)) + end + end + end + end + end + end + end + printDetails(('end countForbiddenEggsForRaceInClaimedNestobxes')) + return eggs_count +end --countForbiddenEggsForRaceInClaimedNestobxes +--------------------------------------------------------------------------------------------------- +function validateEggs(eggs) + if not eggs.egg_flags.fertile then + printDetails('Newly laid eggs are not fertile, do nothing') + return false + end + + local should_be_nestbox = dfhack.items.getHolderBuilding(eggs) + if should_be_nestbox ~= nil then + for _, nestbox in ipairs(df.global.world.buildings.other.NEST_BOX) do + if nestbox == should_be_nestbox then + printDetails('Found nestbox, continue with egg handling') + return true + end + end + printDetails('Newly laid eggs are in building different than nestbox, we were to late') + return false + else + printDetails('Newly laid eggs are not in building, we were to late') + return false + end + return true +end --validateEggs +--------------------------------------------------------------------------------------------------- +function handleEggs(eggs, base_target, add_missing_animals, split_stacks, missing_animals_count) + printDetails(('start handleEggs')) + + local race = eggs.race + printDetails(('Handling eggs for race %s'):format(race)) + printDetails(('base_target: %s'):format(base_target)) + printDetails(('add_missing_animals: %s'):format(race)) + printDetails(('split_stacks: %s'):format(tostring(split_stacks))) + printDetails(('missing_animals_count: %s'):format(tostring(missing_animals_count))) + local target_eggs = base_target + local creature = df.creature_raw.find(eggs.race).creature_id + + if add_missing_animals then + printDetails(('adding missing animal count %s to target'):format(missing_animals_count)) + target_eggs = target_eggs + missing_animals_count + end + + local current_eggs = eggs.stack_size + + local total_count = current_eggs + total_count = total_count + countForbiddenEggsForRaceInClaimedNestobxes(race) + printLocal(("total_count %s"):format(total_count)); + printLocal(("current_eggs %s"):format(current_eggs)); + printLocal(("target_eggs %s"):format(target_eggs)); + + printLocal(('Total count for %s egg(s) is %s'):format(creature, total_count)) + + if total_count - current_eggs < target_eggs then + local egg_count_to_leave_in_source_stack = current_eggs + + if split_stacks and total_count > target_eggs then + printLocal(("1 total_count %s"):format(total_count)); + printLocal(("1 current_eggs %s"):format(current_eggs)); + printLocal(("1 target_eggs %s"):format(target_eggs)); + egg_count_to_leave_in_source_stack = target_eggs - total_count + current_eggs + printLocal(("egg_count_to_leave_in_source_stack %s"):format(egg_count_to_leave_in_source_stack)); + splitEggStack(eggs, egg_count_to_leave_in_source_stack) + end + + eggs.flags.forbid = true + + if eggs.flags.in_job then + local job_ref = dfhack.items.getSpecificRef(eggs, df.specific_ref_type.JOB) + if job_ref then + printDetails(('About to remove job related to egg(s)')) + dfhack.job.removeJob(job_ref.data.job) + eggs.flags.in_job = false + end + end + + printLocal( + ('Previously existing %s egg(s) %s is lower than maximum %s (%s base target + %s missing animals), forbidden %s egg(s) out of %s new'):format( + creature, + total_count - current_eggs, + target_eggs, + base_target, + missing_animals_count, + egg_count_to_leave_in_source_stack, + current_eggs + ) + ) + else + printLocal( + ('Total count for %s egg(s) %s is over maximum %s (%s base target + %s missing animals), newly laid egg(s) %s , no action taken.'):format( + creature, + total_count, + target_eggs, + base_target, + missing_animals_count, + current_eggs + ) + ) + end + printDetails(('end handleEggs')) +end --handleEggs +--------------------------------------------------------------------------------------------------- +return _ENV diff --git a/plugins/nestboxes.cpp b/plugins/nestboxes.cpp deleted file mode 100644 index 6f21628170..0000000000 --- a/plugins/nestboxes.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include "Debug.h" -#include "PluginManager.h" - -#include "modules/Items.h" -#include "modules/Job.h" -#include "modules/Persistence.h" -#include "modules/World.h" - -#include "df/building_nest_boxst.h" -#include "df/item.h" -#include "df/item_eggst.h" -#include "df/unit.h" -#include "df/world.h" - -using std::string; -using namespace DFHack; -using namespace df::enums; - -DFHACK_PLUGIN("nestboxes"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -REQUIRE_GLOBAL(world); - -namespace DFHack { - // for configuration-related logging - DBG_DECLARE(nestboxes, control, DebugCategory::LINFO); - // for logging during the periodic scan - DBG_DECLARE(nestboxes, cycle, DebugCategory::LINFO); -} - -static const string CONFIG_KEY = string(plugin_name) + "/config"; -static PersistentDataItem config; - -enum ConfigValues { - CONFIG_IS_ENABLED = 0, -}; - -static const int32_t CYCLE_TICKS = 7; // need to react quickly when eggs are laid/unforbidden -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - -static void do_cycle(color_ostream &out); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(control,out).print("initializing %s\n", plugin_name); - - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } - - if (enable != is_enabled) { - is_enabled = enable; - DEBUG(control,out).print("%s from the API; persisting\n", - is_enabled ? "enabled" : "disabled"); - config.set_bool(CONFIG_IS_ENABLED, is_enabled); - if (enable) - do_cycle(out); - } else { - DEBUG(control,out).print("%s from the API, but already %s; no action\n", - is_enabled ? "enabled" : "disabled", - is_enabled ? "enabled" : "disabled"); - } - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(control,out).print("shutting down %s\n", plugin_name); - - return CR_OK; -} - -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - cycle_timestamp = 0; - config = World::GetPersistentSiteData(CONFIG_KEY); - - if (!config.isValid()) { - DEBUG(control,out).print("no config found in this save; initializing\n"); - config = World::AddPersistentSiteData(CONFIG_KEY); - config.set_bool(CONFIG_IS_ENABLED, is_enabled); - } - - is_enabled = config.get_bool(CONFIG_IS_ENABLED); - DEBUG(control,out).print("loading persisted enabled state: %s\n", - is_enabled ? "true" : "false"); - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED) { - if (is_enabled) { - DEBUG(control,out).print("world unloaded; disabling %s\n", - plugin_name); - is_enabled = false; - } - } - return CR_OK; -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) - do_cycle(out); - return CR_OK; -} - -///////////////////////////////////////////////////// -// cycle logic -// - -static void do_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); - - // mark that we have recently run - cycle_timestamp = world->frame_counter; - - for (df::building_nest_boxst *nb : world->buildings.other.NEST_BOX) { - bool fertile = false; - if (nb->claimed_by != -1) { - df::unit *u = df::unit::find(nb->claimed_by); - if (u && u->pregnancy_timer > 0) - fertile = true; - } - for (auto &contained_item : nb->contained_items) { - auto *item = virtual_cast(contained_item->item); - if (item && item->flags.bits.forbid != fertile) { - item->flags.bits.forbid = fertile; - if (fertile && item->flags.bits.in_job) { - // cancel any job involving the egg - df::specific_ref *sref = Items::getSpecificRef( - item, df::specific_ref_type::JOB); - if (sref && sref->data.job) - Job::removeJob(sref->data.job); - } - out.print("%d eggs %s.\n", item->getStackSize(), fertile ? "forbidden" : "unforbidden"); - } - } - } -}