diff --git a/assign-minecarts.lua b/assign-minecarts.lua new file mode 100644 index 0000000000..9c859450d6 --- /dev/null +++ b/assign-minecarts.lua @@ -0,0 +1,203 @@ +-- assigns minecarts to hauling routes +--@ module = true +--[====[ + +assign-minecarts +================ +This script allows you to assign minecarts to hauling routes without having to +use the in-game interface. + +Usage:: + + assign-minecarts list|all| [-q|--quiet] + +:list: will show you information about your hauling routes, including whether + they have minecarts assigned to them. +:all: will automatically assign a free minecart to all hauling routes that don't + have a minecart assigned to them. + +If you specifiy a route id, only that route will get a minecart assigned to it +(if it doesn't already have one and there is a free minecart available). + +Add ``-q`` or ``--quiet`` to suppress informational output. + +Note that a hauling route must have at least one stop defined before a minecart +can be assigned to it. +]====] + +local argparse = require('argparse') +local quickfort = reqscript('quickfort') + +-- ensures the list of available minecarts has been calculated by the game +local function refresh_ui_hauling_vehicles() + local qfdata + if #df.global.ui.hauling.routes > 0 then + -- if there is an existing route, move to the vehicle screen and back + -- out to force the game to scan for assignable minecarts + qfdata = 'hv^^' + else + -- if no current routes, create a route, move to the vehicle screen, + -- back out, and remove the route. The extra "px" is in the string in + -- case the user has the confirm plugin enabled. "p" pauses the plugin + -- and "x" retries the route deletion. + qfdata = 'hrv^xpx^' + end + quickfort.apply_blueprint{mode='config', data=qfdata} +end + +function get_free_vehicles() + refresh_ui_hauling_vehicles() + local free_vehicles = {} + for _,minecart in ipairs(df.global.ui.hauling.vehicles) do + if minecart and minecart.route_id == -1 then + table.insert(free_vehicles, minecart) + end + end + return free_vehicles +end + +local function has_minecart(route) + return #route.vehicle_ids > 0 +end + +local function has_stops(route) + return #route.stops > 0 +end + +local function get_name(route) + return route.name and #route.name > 0 and route.name or ('Route '..route.id) +end + +local function get_id_and_name(route) + return ('%d (%s)'):format(route.id, get_name(route)) +end + +local function assign_minecart_to_route(route, quiet, minecart) + if has_minecart(route) then + return true + end + if not has_stops(route) then + if not quiet then + dfhack.printerr( + ('Route %s has no stops defined. Cannot assign minecart.') + :format(get_id_and_name(route))) + end + return false + end + if not minecart then + minecart = get_free_vehicles()[1] + if not minecart then + if not quiet then + dfhack.printerr('No minecarts available! Please build some.') + end + return false + end + end + route.vehicle_ids:insert('#', minecart.id) + route.vehicle_stops:insert('#', 0) + minecart.route_id = route.id + if not quiet then + print(('Assigned a minecart to route %s.') + :format(get_id_and_name(route))) + end + return true +end + +-- assign first free minecart to the most recently-created route +-- returns whether route now has a minecart assigned +function assign_minecart_to_last_route(quiet) + local routes = df.global.ui.hauling.routes + local route_idx = #routes - 1 + if route_idx < 0 then + return false + end + local route = routes[route_idx] + return assign_minecart_to_route(route, quiet) +end + +local function get_route_by_id(route_id) + for _,route in ipairs(df.global.ui.hauling.routes) do + if route.id == route_id then + return route + end + end +end + +local function list() + local routes = df.global.ui.hauling.routes + if 0 == #routes then + print('No hauling routes defined.') + else + print(('Found %d route%s:\n') + :format(#routes, #routes == 1 and '' or 's')) + print('route id minecart? has stops? route name') + print('-------- --------- ---------- ----------') + for _,route in ipairs(routes) do + print(('%-8d %-9s %-9s %s') + :format(route.id, + has_minecart(route) and 'yes' or 'NO', + has_stops(route) and 'yes' or 'NO', + get_name(route))) + end + end + local minecarts = get_free_vehicles() + print(('\nYou have %d unassigned minecart%s.') + :format(#minecarts, #minecarts == 1 and '' or 's')) +end + +local function all(quiet) + local minecarts, idx = get_free_vehicles(), 1 + local routes = df.global.ui.hauling.routes + for _,route in ipairs(routes) do + if has_minecart(route) then + goto continue + end + if not assign_minecart_to_route(route, quiet, minecarts[idx]) then + return + end + idx = idx + 1 + ::continue:: + end +end + +local function do_help() + print(dfhack.script_help()) +end + +local command_switch = { + list=list, + all=all, +} + +local function main(args) + local help, quiet = false, false + local command = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() help = true end}, + {'q', 'quiet', handler=function() quiet = true end}})[1] + + if help then + command = nil + end + + local requested_route_id = tonumber(command) + if requested_route_id then + local route = get_route_by_id(requested_route_id) + if not route then + dfhack.printerr('route id not found: '..requested_route_id) + elseif has_minecart(route) then + if not quiet then + print(('Route %s already has a minecart assigned.') + :format(get_id_and_name(route))) + end + else + assign_minecart_to_route(route, quiet) + end + return + end + + (command_switch[command] or do_help)(quiet) +end + +if not dfhack_flags.module then + main({...}) +end diff --git a/changelog.txt b/changelog.txt index c9137298dc..eecc0edad7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,8 +15,10 @@ that repo. ## New Scripts +- `assign-minecarts`: assign minecarts to hauling routes that don't have one - `deteriorate`: combines, replaces, and extends previous `deteriorateclothes`, `deterioratecorpses`, and `deterioratefood` scripts. - `gui/petitions`: shows list of fort's petitions +- `gui/quantum`: interactive tool for creating quantum stockpiles - `modtools/fire-rate`: allows modders to adjust the rate of fire for ranged attacks ## Fixes diff --git a/gui/quantum.lua b/gui/quantum.lua new file mode 100644 index 0000000000..9dad8b8dab --- /dev/null +++ b/gui/quantum.lua @@ -0,0 +1,388 @@ +-- interactively creates quantum stockpiles +--@ module = true +--[====[ + +gui/quantum +=========== +This script provides a visual, interactive interface to make setting up quantum +stockpiles much easier. + +Quantum stockpiles simplify fort management by allowing a small stockpile to +contain an infinite number of items. This reduces the complexity of your storage +design, lets your dwarves be more efficient, and increases FPS. + +Quantum stockpiles work by linking a "feeder" stockpile to a one-tile minecart +hauling route. As soon as an item from the feeder stockpile is placed in the +minecart, the minecart is tipped and all items land on an adjacent tile. The +single-tile stockpile in that adjacent tile that holds all the items is your +quantum stockpile. + +Before you run this script, create and configure your "feeder" stockpile. The +size of the stockpile determines how many dwarves can be tasked with bringing +items to this quantum stockpile. Somewhere between 1x3 and 5x5 is usually a good +size. + +The script will walk you through the steps: +1) Select the feeder stockpile +2) Configure your quantum stockpile with the onscreen options +3) Select a spot on the map to build the quantum stockpile + +If there are any minecarts available, one will be automatically associated with +the hauling route. If you don't have a free minecart, ``gui/quantum`` will +enqueue a manager order to make one for you. Once it is built, run +``assign-minecarts all`` to assign it to the route, or enter the (h)auling menu +and assign one manually. The quantum stockpile needs a minecart to function. + +Quantum stockpiles work much more efficiently if you add the following line to +your ``onMapLoad.init`` file:: + + prioritize -a StoreItemInVehicle + +This prioritizes moving of items from the feeder stockpile to the minecart. +Otherwise, the feeder stockpile can get full and block the quantum pipeline. + +See :wiki:`the wiki ` for more information on quantum +stockpiles. +]====] + +local dialogs = require('gui.dialogs') +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local widgets = require('gui.widgets') + +local assign_minecarts = reqscript('assign-minecarts') +local quickfort = reqscript('quickfort') +local quickfort_command = reqscript('internal/quickfort/command') +local quickfort_orders = reqscript('internal/quickfort/orders') + +QuantumUI = defclass(QuantumUI, guidm.MenuOverlay) +QuantumUI.ATTRS { + frame_inset=1, + focus_path='quantum', + sidebar_mode=df.ui_sidebar_mode.LookAround, +} + +function QuantumUI:init() + local cart_count = #assign_minecarts.get_free_vehicles() + + local main_panel = widgets.Panel{autoarrange_subviews=true, + autoarrange_gap=1} + main_panel:addviews{ + widgets.Label{text='Quantum'}, + widgets.WrappedLabel{ + text_to_wrap=self:callback('get_help_text'), + text_pen=COLOR_GREY}, + widgets.ResizingPanel{autoarrange_subviews=true, subviews={ + widgets.EditField{ + view_id='name', + key='CUSTOM_N', + on_char=self:callback('on_name_char'), + text=''}, + widgets.TooltipLabel{ + text_to_wrap='Give the quantum stockpile a custom name.', + show_tooltip=true}}}, + widgets.ResizingPanel{autoarrange_subviews=true, subviews={ + widgets.CycleHotkeyLabel{ + view_id='dir', + key='CUSTOM_D', + options={{label='North', value={y=-1}}, + {label='South', value={y=1}}, + {label='East', value={x=1}}, + {label='West', value={x=-1}}}}, + widgets.TooltipLabel{ + text_to_wrap='Set the dump direction of the quantum stop.', + show_tooltip=true}}}, + widgets.WrappedLabel{ + text_to_wrap=('%d minecart%s available: %s will be %s'):format( + cart_count, cart_count == 1 and '' or 's', + cart_count == 1 and 'it' or 'one', + cart_count > 0 and 'automatically assigned to the quantum route' + or 'ordered via the manager for you to assign later')}, + widgets.HotkeyLabel{ + key='LEAVESCREEN', + label=self:callback('get_back_text'), + on_activate=self:callback('on_back')} + } + + self:addviews{main_panel} +end + +function QuantumUI:get_help_text() + if not self.feeder then + return 'Please select the feeder stockpile with the cursor or mouse.' + end + return 'Please select the location of the new quantum stockpile with the' .. + ' cursor or mouse.' +end + +function QuantumUI:get_back_text() + if self.feeder then + return 'Cancel selection' + end + return 'Back' +end + +function QuantumUI:on_back() + if self.feeder then + self.feeder = nil + self:updateLayout() + else + self:dismiss() + end +end + +function QuantumUI:on_name_char(char, text) + return #text < 12 +end + +local function is_in_extents(bld, x, y) + local extents = bld.room.extents + if not extents then return true end -- building is solid + local yoff = (y - bld.y1) * (bld.x2 - bld.x1 + 1) + local xoff = x - bld.x1 + return extents[yoff+xoff] == 1 +end + +function QuantumUI:select_stockpile(pos) + local flags, occupancy = dfhack.maps.getTileFlags(pos) + if not flags or occupancy.building == 0 then return end + local bld = dfhack.buildings.findAtTile(pos) + if not bld or bld:getType() ~= df.building_type.Stockpile then return end + + local tiles = {} + + for x=bld.x1,bld.x2 do + for y=bld.y1,bld.y2 do + if is_in_extents(bld, x, y) then + ensure_key(ensure_key(tiles, bld.z), y)[x] = true + end + end + end + + self.feeder = bld + self.feeder_tiles = tiles + + self:updateLayout() +end + +function QuantumUI:render_feeder_overlay() + if not gui.blink_visible(1000) then return end + + local zlevel = self.feeder_tiles[df.global.window_z] + if not zlevel then return end + + local function get_feeder_overlay_char(pos) + return safe_index(zlevel, pos.y, pos.x) and 'X' + end + + self:renderMapOverlay(get_feeder_overlay_char, self.feeder) +end + +function QuantumUI:get_qsp_pos(cursor) + local offsets = self.subviews.dir:getOptionValue() + return { + x = cursor.x + (offsets.x or 0), + y = cursor.y + (offsets.y or 0), + z = cursor.z + } +end + +local function is_valid_pos(cursor, qsp_pos) + local stats = quickfort.apply_blueprint{mode='place', data='c', pos=qsp_pos, + dry_run=true} + local ok = stats.place_designated.value > 0 + + if ok then + stats = quickfort.apply_blueprint{mode='build', data='trackstop', + pos=cursor, dry_run=true} + ok = stats.build_designated.value > 0 + end + + return ok +end + +function QuantumUI:render_destination_overlay() + local cursor = guidm.getCursorPos() + local qsp_pos = self:get_qsp_pos(cursor) + local bounds = {x1=qsp_pos.x, x2=qsp_pos.x, y1=qsp_pos.y, y2=qsp_pos.y} + + local ok = is_valid_pos(cursor, qsp_pos) + + local function get_dest_overlay_char() + return 'X', ok and COLOR_GREEN or COLOR_RED + end + + self:renderMapOverlay(get_dest_overlay_char, bounds) +end + +function QuantumUI:onRenderBody() + if not self.feeder then return end + + self:render_feeder_overlay() + self:render_destination_overlay() +end + +function QuantumUI:onInput(keys) + if self:inputToSubviews(keys) then return true end + + self:propagateMoveKeys(keys) + + local pos = nil + if keys._MOUSE_L then + local x, y = dfhack.screen.getMousePos() + if gui.is_in_rect(self.df_layout.map, x, y) then + pos = xyz2pos(df.global.window_x + x - 1, + df.global.window_y + y - 1, + df.global.window_z) + guidm.setCursorPos(pos) + end + elseif keys.SELECT then + pos = guidm.getCursorPos() + end + + if pos then + if not self.feeder then + self:select_stockpile(pos) + else + local qsp_pos = self:get_qsp_pos(pos) + if not is_valid_pos(pos, qsp_pos) then + return + end + + self:dismiss() + self:commit(pos, qsp_pos) + end + end +end + +local function get_feeder_pos(feeder_tiles) + for z,rows in pairs(feeder_tiles) do + for y,row in pairs(rows) do + for x in pairs(row) do + return xyz2pos(x, y, z) + end + end + end +end + +local function get_moves(move, move_back, start_pos, end_pos, + move_to_greater_token, move_to_less_token) + if start_pos == end_pos then + return move, move_back + end + local diff = math.abs(start_pos - end_pos) + local move_to_greater_pattern = ('{%s %%d}'):format(move_to_greater_token) + local move_to_greater = move_to_greater_pattern:format(diff) + local move_to_less_pattern = ('{%s %%d}'):format(move_to_less_token) + local move_to_less = move_to_less_pattern:format(diff) + if start_pos < end_pos then + return move..move_to_greater, move_back..move_to_less + end + return move..move_to_less, move_back..move_to_greater +end + +local function get_quantumstop_data(dump_pos, feeder_pos, name) + local move, move_back = get_moves('', '', dump_pos.z, feeder_pos.z, '<','>') + move, move_back = get_moves(move, move_back, dump_pos.y, feeder_pos.y, + 'Down', 'Up') + move, move_back = get_moves(move, move_back, dump_pos.x, feeder_pos.x, + 'Right', 'Left') + + local quantumstop_name_part, quantum_name_part = '', '' + if name ~= '' then + quantumstop_name_part = (' name="%s quantum"'):format(name) + quantum_name_part = ('{givename name="%s dumper"}'):format(name) + end + + return ('{quantumstop%s move="%s" move_back="%s"}%s') + :format(quantumstop_name_part, move, move_back, quantum_name_part) +end + +local function get_quantum_data(name) + local name_part = '' + if name ~= '' then + name_part = (' name="%s"'):format(name) + end + return ('{quantum%s}'):format(name_part) +end + +local function order_minecart(pos) + local quickfort_ctx = quickfort_command.init_ctx{ + command='orders', blueprint_name='gui/quantum', cursor=pos} + quickfort_orders.enqueue_additional_order(quickfort_ctx, 'wooden minecart') + quickfort_orders.create_orders(quickfort_ctx) +end + +local function create_quantum(pos, qsp_pos, feeder_tiles, name, trackstop_dir) + local stats = quickfort.apply_blueprint{mode='place', data='c', pos=qsp_pos} + if stats.place_designated.value == 0 then + error(('failed to place quantum stockpile at (%d, %d, %d)') + :format(qsp_pos.x, qsp_pos.y, qsp_pos.z)) + end + + stats = quickfort.apply_blueprint{mode='build', + data='trackstop'..trackstop_dir, pos=pos} + if stats.build_designated.value == 0 then + error(('failed to build trackstop at (%d, %d, %d)') + :format(pos.x, pos.y, pos.z)) + end + + local feeder_pos = get_feeder_pos(feeder_tiles) + local quantumstop_data = get_quantumstop_data(pos, feeder_pos, name) + stats = quickfort.apply_blueprint{mode='query', data=quantumstop_data, + pos=pos} + if stats.query_skipped_tiles.value > 0 then + error(('failed to query trackstop at (%d, %d, %d)') + :format(pos.x, pos.y, pos.z)) + end + + local quantum_data = get_quantum_data(name) + stats = quickfort.apply_blueprint{mode='query', data=quantum_data, + pos=qsp_pos} + if stats.query_skipped_tiles.value > 0 then + error(('failed to query quantum stockpile at (%d, %d, %d)') + :format(qsp_pos.x, qsp_pos.y, qsp_pos.z)) + end +end + +-- this function assumes that is_valid_pos() has already validated the positions +function QuantumUI:commit(pos, qsp_pos) + local name = self.subviews.name.text + local trackstop_dir = self.subviews.dir:getOptionLabel():sub(1,1) + create_quantum(pos, qsp_pos, self.feeder_tiles, name, trackstop_dir) + + local message = nil + if assign_minecarts.assign_minecart_to_last_route(true) then + message = 'An available minecart was assigned to your new' .. + ' quantum stockpile. You\'re all done!' + else + order_minecart(pos) + message = 'There are no minecarts available to assign to the' .. + ' quantum stockpile, but a manager order to produce' .. + ' one was created for you. Once the minecart is' .. + ' built, please add it to the quantum stockpile route' .. + ' with the "assign-minecarts all" command or manually in' .. + ' the (h)auling menu.' + end + -- display a message box telling the user what we just did + dialogs.MessageBox{text=message:wrap(70)}:show() +end + +if dfhack.internal.IN_TEST then + unit_test_hooks = { + is_in_extents=is_in_extents, + is_valid_pos=is_valid_pos, + get_feeder_pos=get_feeder_pos, + get_moves=get_moves, + get_quantumstop_data=get_quantumstop_data, + get_quantum_data=get_quantum_data, + create_quantum=create_quantum, + } +end + +if dfhack_flags.module then + return +end + +view = QuantumUI{} +view:show() diff --git a/internal/quickfort/orders.lua b/internal/quickfort/orders.lua index 41d903505c..a0defe8b3f 100644 --- a/internal/quickfort/orders.lua +++ b/internal/quickfort/orders.lua @@ -138,9 +138,19 @@ local function get_reactions() return g_reactions end -function enqueue_building_orders(buildings, building_db, ctx) +local function ensure_order_specs(ctx) local order_specs = ctx.order_specs or {} ctx.order_specs = order_specs + return order_specs +end + +function enqueue_additional_order(ctx, label) + local order_specs = ensure_order_specs(ctx) + inc_order_spec(order_specs, 1, get_reactions(), label) +end + +function enqueue_building_orders(buildings, building_db, ctx) + local order_specs = ensure_order_specs(ctx) local reactions = get_reactions() for _, b in ipairs(buildings) do local db_entry = building_db[b.type] @@ -156,17 +166,13 @@ function enqueue_building_orders(buildings, building_db, ctx) end if db_entry.additional_orders then for _,label in ipairs(db_entry.additional_orders) do - local quantity = 1 - if additional_order == df.item_type.BLOCKS then - quantity = 1 / 4 - end - inc_order_spec(order_specs, quantity, reactions, label) + inc_order_spec(order_specs, 1, reactions, label) end end for _,filter in ipairs(filters) do if filter.quantity == -1 then filter.quantity = get_num_items(b) end if filter.flags2 and filter.flags2.building_material then - -- blocks get produced at a ratio of 4:1 + -- rock blocks get produced at a ratio of 4:1 filter.quantity = filter.quantity or 1 filter.quantity = filter.quantity / 4 end diff --git a/internal/quickfort/set.lua b/internal/quickfort/set.lua index c93e0ab624..dd56b8fc77 100644 --- a/internal/quickfort/set.lua +++ b/internal/quickfort/set.lua @@ -60,8 +60,6 @@ local function set_setting(key, value) end local function read_settings(reader) - print(string.format('reading quickfort configuration from "%s"', - reader.filepath)) local line = reader:get_next_row() while line do local _, _, key, value = string.find(line, '^%s*([%a_]+)%s*=%s*(%S.*)') @@ -116,6 +114,8 @@ function do_set(args) end function do_reset() + print(string.format('reading quickfort configuration from "%s"', + config_file)) local get_reader_fn = function() return quickfort_reader.TextReader{filepath=config_file} end diff --git a/test/assign-minecarts.lua b/test/assign-minecarts.lua new file mode 100644 index 0000000000..2dc2829dd0 --- /dev/null +++ b/test/assign-minecarts.lua @@ -0,0 +1,232 @@ +local am = reqscript('assign-minecarts') + +local quickfort = reqscript('quickfort') + +local mock_routes, mock_vehicles +local mock_apply_blueprint, mock_print, mock_script_help + +config.wrapper = function(test_fn) + mock_routes, mock_vehicles = {}, {} + + local mock_df = {} + mock_df.global = {} + mock_df.global.ui = {} + mock_df.global.ui.hauling = {} + mock_df.global.ui.hauling.routes = mock_routes + mock_df.global.ui.hauling.vehicles = mock_vehicles + + mock_apply_blueprint, mock_print = mock.func(), mock.func() + mock_script_help = mock.func() + + mock.patch({{am, 'df', mock_df}, + {quickfort, 'apply_blueprint', mock_apply_blueprint}, + {am.dfhack, 'script_help', mock_script_help}, + {am, 'print', mock_print}}, + test_fn) +end + +function test.get_free_vehicles_no_routes() + am.get_free_vehicles() + expect.str_find('x', mock_apply_blueprint.call_args[1][1].data, + 'should attempt to remove a route') +end + +function test.get_free_vehicles_existing_routes() + mock_routes[1] = {} + am.get_free_vehicles() + expect.str_find('v^^', mock_apply_blueprint.call_args[1][1].data, + 'should not attempt to remove a route') +end + +function test.get_free_vehicles_no_vehicles() + expect.eq(0, #am.get_free_vehicles()) +end + +function test.get_free_vehicles_no_free_vehicles() + mock_vehicles[1] = {route_id=10} + mock_vehicles[2] = {route_id=11} + expect.eq(0, #am.get_free_vehicles()) +end + +function test.get_free_vehicles_has_free_vehicles() + mock_vehicles[1] = {route_id=10} + mock_vehicles[2] = {route_id=-1} + mock_vehicles[3] = {route_id=11} + local free_vehicles = am.get_free_vehicles() + expect.eq(1, #free_vehicles) + expect.eq(mock_vehicles[2], free_vehicles[1]) +end + +function test.assign_minecart_to_last_route_no_routes() + expect.false_(am.assign_minecart_to_last_route(true)) + expect.eq(0, mock_print.call_count) +end + +function test.assign_minecart_to_last_route_already_has_minecart() + mock_routes[1] = {stops={[1]={}}, vehicle_ids={10}} + mock_routes[0] = mock_routes[1] -- simulate 0-based index + expect.true_(am.assign_minecart_to_last_route(true)) + expect.eq(0, mock_print.call_count) +end + +function test.assign_minecart_to_last_route_no_stops() + mock_routes[1] = {stops={}, vehicle_ids={}} + mock_routes[0] = mock_routes[1] -- simulate 0-based index + expect.false_(am.assign_minecart_to_last_route(true)) + expect.eq(0, mock_print.call_count) +end + +function test.assign_minecart_to_last_route_no_stops_output() + mock_routes[1] = {id=10, stops={}, vehicle_ids={}} + mock_routes[0] = mock_routes[1] -- simulate 0-based index + expect.printerr_match('no stops defined', + function() expect.false_(am.assign_minecart_to_last_route(false)) end) +end + +function test.assign_minecart_to_last_route_no_minecarts() + mock_routes[1] = {stops={[1]={}}, vehicle_ids={}} + mock_routes[0] = mock_routes[1] -- simulate 0-based index + expect.false_(am.assign_minecart_to_last_route(true)) + expect.eq(0, mock_print.call_count) +end + +function test.assign_minecart_to_last_route_no_minecarts() + mock_routes[1] = {id=10, stops={[1]={}}, vehicle_ids={}} + mock_routes[0] = mock_routes[1] -- simulate 0-based index + expect.printerr_match('No minecarts available', + function() expect.false_(am.assign_minecart_to_last_route(false)) end) +end + +local function fake_insert(self, pos, value) + expect.eq('#', pos) + table.insert(self, value) +end + +function test.assign_minecart_to_last_route_happy() + mock_vehicles[1] = {id=100, route_id=-1} + mock_routes[1] = {id=5, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}} + mock_routes[0] = mock_routes[1] -- simulate 0-based index + expect.true_(am.assign_minecart_to_last_route(true)) + expect.eq(1, #mock_routes[1].vehicle_ids) + expect.eq(100, mock_routes[1].vehicle_ids[1]) + expect.eq(1, #mock_routes[1].vehicle_stops) + expect.eq(0, mock_routes[1].vehicle_stops[1]) + expect.eq(5, mock_vehicles[1].route_id) + expect.eq(0, mock_print.call_count) +end + +function test.main_all_more_routes_than_vehicles() + mock_vehicles[1] = {id=100, route_id=-1} + mock_vehicles[2] = {id=200, route_id=-1} + mock_routes[1] = {id=10, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}} + mock_routes[2] = {id=20, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}} + mock_routes[3] = {id=30, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}} + + dfhack.run_script('assign-minecarts', 'all', '-q') + + expect.eq(100, mock_routes[1].vehicle_ids[1]) + expect.eq(10, mock_vehicles[1].route_id) + expect.eq(200, mock_routes[2].vehicle_ids[1]) + expect.eq(20, mock_vehicles[2].route_id) + expect.eq(0, #mock_routes[3].vehicle_ids) + + expect.eq(0, mock_print.call_count) +end + +function test.main_all_more_vehicles_than_routes() + mock_vehicles[1] = {id=100, route_id=-1} + mock_vehicles[2] = {id=200, route_id=-1} + mock_vehicles[3] = {id=300, route_id=-1} + mock_routes[1] = {id=10, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}} + mock_routes[2] = {id=20, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}} + + dfhack.run_script('assign-minecarts', 'all', '-q') + + expect.eq(100, mock_routes[1].vehicle_ids[1]) + expect.eq(10, mock_vehicles[1].route_id) + expect.eq(200, mock_routes[2].vehicle_ids[1]) + expect.eq(20, mock_vehicles[2].route_id) + expect.eq(-1, mock_vehicles[3].route_id) + + expect.eq(0, mock_print.call_count) +end + +function test.main_list_no_routes_no_minecarts() + dfhack.run_script('assign-minecarts', 'list') + + expect.eq(2, mock_print.call_count) + expect.str_find('No hauling routes', mock_print.call_args[1][1]) + expect.str_find('0 unassigned minecarts', mock_print.call_args[2][1]) +end + +function test.main_list_happy() + mock_vehicles[1] = {id=100, route_id=-1} + mock_vehicles[2] = {id=200, route_id=20} + mock_routes[1] = {id=10, stops={[1]={}}, vehicle_ids={}, + vehicle_stops={}, name='goober'} + mock_routes[2] = {id=20, stops={[1]={}}, vehicle_ids={20}, + vehicle_stops={0}} + + dfhack.run_script('assign-minecarts', 'list') + + expect.eq(6, mock_print.call_count) + expect.str_find('Found 2 routes', mock_print.call_args[1][1]) + expect.str_find('10%s+NO%s+yes%s+goober', mock_print.call_args[4][1]) + expect.str_find('20%s+yes%s+yes%s+Route 20', mock_print.call_args[5][1]) + expect.str_find('1 unassigned minecart', mock_print.call_args[6][1]) +end + +function test.main_route_id_not_exist() + expect.printerr_match('route id not found', + function() dfhack.run_script('assign-minecarts', '1000') end) +end + +function test.main_route_id_already_has_minecart() + mock_vehicles[1] = {id=100, route_id=-1} + mock_routes[1] = {id=10, stops={[1]={}}, vehicle_ids={100}, + vehicle_stops={0}} + + dfhack.run_script('assign-minecarts', '10') + + expect.eq(1, mock_print.call_count) + expect.str_find('already has a minecart', mock_print.call_args[1][1]) +end + +function test.main_route_id_happy() + mock_vehicles[1] = {id=100, route_id=-1} + mock_routes[1] = {id=10, stops={[1]={}}, vehicle_ids={insert=fake_insert}, + vehicle_stops={insert=fake_insert}, name='dumper'} + + dfhack.run_script('assign-minecarts', '10') + + expect.eq(100, mock_routes[1].vehicle_ids[1]) + expect.eq(10, mock_vehicles[1].route_id) + expect.eq(1, mock_print.call_count) + expect.str_find('Assigned a minecart', mock_print.call_args[1][1]) +end + +function test.main_help_no_args() + dfhack.run_script('assign-minecarts') + expect.eq(1, mock_script_help.call_count) +end + +function test.main_help_help() + dfhack.run_script('assign-minecarts', 'help') + expect.eq(1, mock_script_help.call_count) +end + +function test.main_help_help_opt() + dfhack.run_script('assign-minecarts', '--help') + expect.eq(1, mock_script_help.call_count) +end + +function test.main_help_command_with_help_opt() + dfhack.run_script('assign-minecarts', 'list', '--help') + expect.eq(1, mock_script_help.call_count) +end diff --git a/test/gui/quantum.lua b/test/gui/quantum.lua new file mode 100644 index 0000000000..c61596c1a2 --- /dev/null +++ b/test/gui/quantum.lua @@ -0,0 +1,150 @@ +local q = reqscript('gui/quantum').unit_test_hooks + +local quickfort = reqscript('quickfort') +local quickfort_building = reqscript('internal/quickfort/building') + +-- Note: the gui_quantum quickfort ecosystem integration test exercises the +-- QuantumUI functions + +function test.is_in_extents() + -- create an upside-down "T" with tiles down the center and bottom + local extent_grid = { + [1]={[5]=true}, + [2]={[5]=true}, + [3]={[1]=true,[2]=true,[3]=true,[4]=true,[5]=true}, + [4]={[5]=true}, + [5]={[5]=true}, + } + local extents = quickfort_building.make_extents( + {width=5, height=5, extent_grid=extent_grid}) + dfhack.with_temp_object(extents, function() + local bld = {x1=10, x2=14, y1=20, room={extents=extents}} + expect.false_(q.is_in_extents(bld, 10, 20)) + expect.false_(q.is_in_extents(bld, 14, 23)) + expect.true_(q.is_in_extents(bld, 12, 20)) + expect.true_(q.is_in_extents(bld, 14, 24)) + end) +end + +function test.is_valid_pos() + local all_good = {place_designated={value=1}, build_designated={value=1}} + local all_bad = {place_designated={value=0}, build_designated={value=0}} + local bad_place = {place_designated={value=0}, build_designated={value=1}} + local bad_build = {place_designated={value=1}, build_designated={value=0}} + + mock.patch(quickfort, 'apply_blueprint', mock.func(all_good), function() + expect.true_(q.is_valid_pos()) end) + mock.patch(quickfort, 'apply_blueprint', mock.func(all_bad), function() + expect.false_(q.is_valid_pos()) end) + mock.patch(quickfort, 'apply_blueprint', mock.func(bad_place), function() + expect.false_(q.is_valid_pos()) end) + mock.patch(quickfort, 'apply_blueprint', mock.func(bad_build), function() + expect.false_(q.is_valid_pos()) end) +end + +function test.get_feeder_pos() + local tiles = {[20]={[30]={[40]=true}}} + expect.table_eq({x=40, y=30, z=20}, q.get_feeder_pos(tiles)) +end + +function test.get_moves() + local move_prefix, move_back_prefix = 'mp', 'mbp' + local increase_token, decrease_token = '+', '-' + + local start_pos, end_pos = 10, 15 + expect.table_eq({'mp{+ 5}', 'mbp{- 5}'}, + {q.get_moves(move_prefix, move_back_prefix, start_pos, end_pos, + increase_token, decrease_token)}) + + start_pos, end_pos = 15, 10 + expect.table_eq({'mp{- 5}', 'mbp{+ 5}'}, + {q.get_moves(move_prefix, move_back_prefix, start_pos, end_pos, + increase_token, decrease_token)}) + + start_pos, end_pos = 10, 10 + expect.table_eq({'mp', 'mbp'}, + {q.get_moves(move_prefix, move_back_prefix, start_pos, end_pos, + increase_token, decrease_token)}) + + start_pos, end_pos = 1, -1 + expect.table_eq({'mp{- 2}', 'mbp{+ 2}'}, + {q.get_moves(move_prefix, move_back_prefix, start_pos, end_pos, + increase_token, decrease_token)}) +end + +function test.get_quantumstop_data() + local dump_pos = {x=40, y=30, z=20} + local feeder_pos = {x=41, y=32, z=23} + local name = '' + expect.eq('{quantumstop move="{< 3}{Down 2}{Right 1}" move_back="{> 3}{Up 2}{Left 1}"}', + q.get_quantumstop_data(dump_pos, feeder_pos, name)) + + name = 'foo' + expect.eq('{quantumstop name="foo quantum" move="{< 3}{Down 2}{Right 1}" move_back="{> 3}{Up 2}{Left 1}"}{givename name="foo dumper"}', + q.get_quantumstop_data(dump_pos, feeder_pos, name)) + + dump_pos = {x=40, y=32, z=20} + feeder_pos = {x=40, y=30, z=20} + name = '' + expect.eq('{quantumstop move="{Up 2}" move_back="{Down 2}"}', + q.get_quantumstop_data(dump_pos, feeder_pos, name)) +end + +function test.get_quantum_data() + expect.eq('{quantum}', q.get_quantum_data('')) + expect.eq('{quantum name="foo"}', q.get_quantum_data('foo')) +end + +function test.create_quantum() + local pos, qsp_pos = {x=1, y=2, z=3}, {x=4, y=5, z=6} + local feeder_tiles = {[0]={[0]={[0]=true}}} + local all_good = {place_designated={value=1}, build_designated={value=1}, + query_skipped_tiles={value=0}} + local bad_place = {place_designated={value=0}, build_designated={value=1}, + query_skipped_tiles={value=0}} + local bad_build = {place_designated={value=1}, build_designated={value=0}, + query_skipped_tiles={value=0}} + local bad_query = {place_designated={value=1}, build_designated={value=1}, + query_skipped_tiles={value=1}} + + local function mock_apply_blueprint(ret_for_pos, ret_for_qsp_pos) + return function(args) + if same_xyz(args.pos, pos) then return ret_for_pos end + return ret_for_qsp_pos + end + end + + mock.patch(quickfort, 'apply_blueprint', + mock_apply_blueprint(all_good, all_good), function() + q.create_quantum(pos, qsp_pos, feeder_tiles, '', 'N') + -- passes if no error is thrown + end) + + mock.patch(quickfort, 'apply_blueprint', + mock_apply_blueprint(all_good, bad_place), function() + expect.error_match('failed to place quantum stockpile', function() + q.create_quantum(pos, qsp_pos, feeder_tiles, '', 'N') + end) + end) + + mock.patch(quickfort, 'apply_blueprint', + mock_apply_blueprint(bad_build, all_good), function() + expect.error_match('failed to build trackstop', function() + q.create_quantum(pos, qsp_pos, feeder_tiles, '', 'N') + end) + end) + + mock.patch(quickfort, 'apply_blueprint', + mock_apply_blueprint(bad_query, all_good), function() + expect.error_match('failed to query trackstop', function() + q.create_quantum(pos, qsp_pos, feeder_tiles, '', 'N') + end) + end) + + mock.patch(quickfort, 'apply_blueprint', + mock_apply_blueprint(all_good, bad_query), function() + expect.error_match('failed to query quantum stockpile', function() + q.create_quantum(pos, qsp_pos, feeder_tiles, '', 'N') + end) + end) +end