diff --git a/Makefile.sh b/Makefile.sh index 0aa1054..f1234dc 100755 --- a/Makefile.sh +++ b/Makefile.sh @@ -231,7 +231,6 @@ fi info "Detected differences between local and deployed directories" if [[ "$ACTION" == "cp" ]]; then - info "Copying . to $DEST_DIR" # Should we create a backup of the deployed directory? if [[ -n "${BACKUP:-}" ]]; then @@ -244,6 +243,7 @@ if [[ "$ACTION" == "cp" ]]; then fi fi + info "Copying . to $DEST_DIR" # Deploy this mod to the destination directory if dry checked rm -r "$DEST_DIR"; then mkdir "$DEST_DIR" 2>/dev/null diff --git a/build/ui.png b/build/ui.png new file mode 100644 index 0000000..350668d Binary files /dev/null and b/build/ui.png differ diff --git a/files/common.lua b/files/common.lua index 7837365..001d0a2 100644 --- a/files/common.lua +++ b/files/common.lua @@ -9,26 +9,49 @@ -- When debugging is enabled and a shift uses a flask for either the -- source or destination material, the other material should not have -- the "(no flask)" designation, as it's redundant. +-- +-- TODO: +-- +-- Move the setting stuff into a class and allow stuff like +-- Config.previous_count.get() +-- Config.previous_count.set(num) +-- or something dofile_once("data/scripts/lib/utilities.lua") dofile_once("mods/shift_query/files/constants.lua") MOD_ID = "shift_query" K_CONFIG_LOG_ENABLE = MOD_ID .. "." .. "q_logging" +K_CONFIG_FORCE_UPDATE = MOD_ID .. "." .. "q_force_update" --- Return either "Enable" or "Disable" based on the condition +--[[ Return either "Enable" or "Disable" based on the condition ]] function f_enable(cond) if cond then return "Enable" end return "Disable" end --- Print a message to both the game and to the console +--[[ Trigger a force update ]] +function q_force_update() + GlobalsSetValue(K_CONFIG_FORCE_UPDATE, FLAG_ON) +end + +--[[ Clear the force update flag ]] +function q_clear_force_update() + GlobalsSetValue(K_CONFIG_FORCE_UPDATE, FLAG_OFF) +end + +--[[ True if an update is being forced ]] +function q_is_update_forced() + return GlobalsGetValue(K_CONFIG_FORCE_UPDATE, FLAG_OFF) ~= FLAG_OFF +end + +--[[ Print a message to both the game and to the console ]] function q_print(msg) GamePrint(msg) print(msg) end --- Format a " pending/previous shift(s)" message +--[[ Format a " pending/previous shift(s)" message ]] function f_shift_count(num, label) local prefix = tostring(num) local suffix = "shifts" @@ -41,12 +64,12 @@ function f_shift_count(num, label) return ("%s %s %s"):format(prefix, label, suffix) end --- Returns true if logging is enabled, false otherwise. +--[[ Returns true if logging is enabled, false otherwise. ]] function q_logging() return GlobalsGetValue(K_CONFIG_LOG_ENABLE, FLAG_OFF) ~= FLAG_OFF end --- Enable or disable logging +--[[ Enable or disable logging ]] function q_set_logging(enable) if enable then GlobalsSetValue(K_CONFIG_LOG_ENABLE, FLAG_ON) @@ -55,16 +78,17 @@ function q_set_logging(enable) q_log("Disabling debugging") GlobalsSetValue(K_CONFIG_LOG_ENABLE, FLAG_OFF) end + q_force_update() end --- Display a logging message if logging is enabled. +--[[ Display a logging message if logging is enabled. ]] function q_log(msg) if q_logging() then q_print("DEBUG: " .. msg) end end --- Display a formatted logging message if logging is enabled. +--[[ Display a formatted logging message if logging is enabled. ]] function q_logf(msg, ...) if q_logging() then q_log(msg:format(...)) @@ -80,6 +104,8 @@ end -- The cache allows for these values to appear instantly. Cache entries -- are stored when setting a value and cleared once that value is -- properly stored. +-- +-- FIXME: Just use ModSettingGetNextValue; this is unnecessary --]] local config_cache = {} @@ -107,6 +133,9 @@ function q_setting_set(setting, value) new_value = value } ModSettingSetNextValue(MOD_ID .. "." .. setting, value, false) + if old_value ~= value then + q_force_update() + end end -- Get the "enable gui?" setting's value @@ -123,24 +152,6 @@ function q_disable_gui() q_setting_set(SETTING_ENABLE, false) end --- Clear the logging global if set -function logger_clear() - local value = GlobalsGetValue("shift_query.logging") or "" - if value ~= "" then - GlobalsSetValue("shift_query.logging", "") - end -end - --- Add a component to the logging global -function logger_add(piece) - local old_log = GlobalsGetValue("shift_query.logging") or "" - local new_log = tostring(piece) - if old_log ~= "" then - new_log = old_log .. "\n" .. new_log - end - GlobalsSetValue("shift_query.logging", new_log) -end - -- Localize a material, either "name" or "$mat_name". function localize_material(material) local matid = CellFactory_GetType(material) @@ -148,11 +159,7 @@ function localize_material(material) if matid ~= -1 then mname = CellFactory_GetUIName(matid) end - local name = GameTextGetTranslatedOrNot(mname) - if not name then -- handle nil - return "" - end - return name + return GameTextGetTranslatedOrNot(mname) or "" end -- Localize a material based on the mode argument @@ -177,13 +184,13 @@ function localize_material_via(material, loc_mode) return ("[%s]"):format(material) end --- Possibly localize a material based on q_logging, localize setting +--[[ Possibly localize a material based on q_logging, localize setting ]] function maybe_localize_material(material) local loc_mode = q_setting_get(SETTING_LOCALIZE) return localize_material_via(material, loc_mode) end --- Format a material with the possibility of including a flask. +--[[ Format a material with the possibility of including a flask. ]] function flask_or(material, use_flask) local logging = q_logging() local mname = maybe_localize_material(material) @@ -196,7 +203,7 @@ function flask_or(material, use_flask) return mname end --- Format a fungal shift. Returns a table of pairs of strings. +--[[ Format a fungal shift. Returns a table of pairs of strings. ]] function format_shift(shift) if not shift then return {{"invalid shift", "invalid shift"}} end local source = shift.from @@ -210,23 +217,23 @@ function format_shift(shift) if not target then s_target = "no data" end - return {s_source, s_target} + return {{s_source, s_target}} end local s_target = flask_or(target.material, target.flask) local material_pairs = {} - if source.name_material then + local want_expand = q_setting_get(SETTING_EXPAND) + if want_expand == EXPAND_ONE and source.name_material then local s_source = flask_or(source.name_material, source.flask) table.insert(material_pairs, {s_source, s_target}) else - for index, material in ipairs(source.materials) do - local s_source = flask_or(material, source.flask) - table.insert(material_pairs, {s_source, s_target}) - end + local s_source = table.concat(source.materials, ", ") + s_source = flask_or(s_source, source.flask) + table.insert(material_pairs, {s_source, s_target}) end return material_pairs end --- Format a number relative to its current value +--[[ Format a number relative to its current value ]] function format_relative(curr, index) local term = "invalid" if index == curr then @@ -244,7 +251,7 @@ function format_relative(curr, index) return term end --- Format a duration of time +--[[ Format a duration of time ]] function format_duration(nsecs) local total = math.abs(nsecs) local hours = math.floor(total / 60 / 60) diff --git a/files/constants.lua b/files/constants.lua index 67522f8..332979c 100644 --- a/files/constants.lua +++ b/files/constants.lua @@ -5,6 +5,8 @@ MAX_SHIFTS = 20 -- the game implicitly supports only 20 shifts ALL_SHIFTS = -1 -- values < 0 mean "all" +CUTOFF_RARE = 0.2 -- shifts with this probability or lower are "rare" + FLAG_ON = "1" FLAG_OFF = "0" @@ -15,16 +17,24 @@ SETTING_PREVIOUS = "previous_count" SETTING_NEXT = "next_count" SETTING_LOCALIZE = "localize" SETTING_ENABLE = "enable" +SETTING_EXPAND = "expand_from" SETTING_APLC = "include_aplc" +SETTING_REAL = "flask_real" CONF_PREVIOUS = ("%s.%s"):format(MOD_ID, SETTING_PREVIOUS) CONF_NEXT = ("%s.%s"):format(MOD_ID, SETTING_NEXT) CONF_LOCALIZE = ("%s.%s"):format(MOD_ID, SETTING_LOCALIZE) CONF_ENABLE = ("%s.%s"):format(MOD_ID, SETTING_ENABLE) +CONF_EXPAND = ("%s.%s"):format(MOD_ID, SETTING_EXPAND) CONF_APLC = ("%s.%s"):format(MOD_ID, SETTING_APLC) +CONF_REAL = ("%s.%s"):format(MOD_ID, SETTING_REAL) --[[ Material formatting rules ]] FORMAT_INTERNAL = "internal" FORMAT_LOCALE = "locale" FORMAT_BOTH = "both" +--[[ Source material expansion rules ]] +EXPAND_ONE = "one" +EXPAND_ALL = "all" + diff --git a/files/materials.lua b/files/materials.lua index 7d3cd57..3484567 100644 --- a/files/materials.lua +++ b/files/materials.lua @@ -1,7 +1,18 @@ -- Fungal Query materials --- The following appears to crash Noita (Dec 21, release) ---dofile("data/scripts/magic/fungal_shift.lua") +--[[ NOTE: +-- While it would be easy just to have +dofile("data/scripts/magic/fungal_shift.lua") +-- and expose those values here; that script (as of Feb 14 2024) crashes Noita +-- if executed during mod initialization. This is because the script attempts +-- to validate the material tables via the CellFactory (CellFactory_GetType) +-- the instant the script is executed. The CellFactory is not initialized by +-- this point and thus the game crashes. +-- +-- To work around this, this file provides two solutions: +-- 1. Load data/scripts/magic/fungal_shift.lua only when requested, and +-- 2. Provide hard-coded material tables if the native ones are unavailable. +--]] -- The tables below are taken directly from Noita's own fungal_shift.lua -- script, with some formatting for good measure. Note that changes to @@ -90,19 +101,23 @@ MATERIALS_TO_COPY = { { probability = 0.01, material = "cheese_static" }, } +function get_material_tables() + dofile("data/scripts/magic/fungal_shift.lua") + if materials_from and materials_to then + return materials_from, materials_to + end + GamePrint("shift_query - unable to get material tables; using local versions") + return MATERIALS_FROM_COPY, MATERIALS_TO_COPY +end + return { --[[ Obtain the materials_from table. -- This first attempts to determine the table directly from Noita, and then -- falls back to the table defined above. --]] get_materials_from = function() - dofile("data/scripts/magic/fungal_shift.lua") - -- luacheck: globals materials_from materials_to - if materials_from and materials_to then - return materials_from - end - GamePrint("shift_query - unable to get source material table; using local copy") - return MATERIALS_FROM_COPY + local materials_from, materials_to = get_material_tables() + return materials_from end, --[[ Obtain the materials_to table. @@ -110,13 +125,42 @@ return { -- falls back to the table defined above. --]] get_materials_to = function() - dofile("data/scripts/magic/fungal_shift.lua") - -- luacheck: globals materials_from materials_to - if materials_from and materials_to then - return materials_to + local materials_from, materials_to = get_material_tables() + return materials_to + end, + + --[[ True if the given shift source is considered "rare" ]] + is_rare_source = function(material, cutoff) + if type(cutoff) ~= "number" then + cutoff = 0.2 + end + local materials_from, materials_to = get_material_tables() + for _, entry in ipairs(materials_from) do + if entry.probability <= cutoff then + for _, shift_mat in ipairs(entry.materials) do + if shift_mat == material then + return true + end + end + end + end + return false + end, + + --[[ True if the given shift destination is considered "rare" ]] + is_rare_target = function(material, cutoff) + if type(cutoff) ~= "number" then + cutoff = 0.2 + end + local materials_from, materials_to = get_material_tables() + for _, entry in ipairs(materials_to) do + if entry.probability <= cutoff then + if entry.material == material then + return true + end + end end - GamePrint("shift_query - unable to get source material table; using local copy") - return MATERIALS_TO_COPY + return false end, --[[ Material tables ]] diff --git a/files/query.lua b/files/query.lua index c8fb79b..28e8394 100644 --- a/files/query.lua +++ b/files/query.lua @@ -1,5 +1,12 @@ --[[ --- Shift Query mod core logic +-- Shift Query core logic +-- +-- Nomenclature used below: +-- +-- shift-info: table +-- flask: boolean +-- materials: {string...} +-- name_material: string (optional) --]] -- luacheck: globals pick_random_from_table_weighted random_nexti random_create @@ -12,44 +19,72 @@ COOLDOWN = 60*60*5 -- post shift cooldown; five minutes -- Get the current shift iteration function get_curr_iter() - return tonumber(GlobalsGetValue("fungal_shift_iteration", "0")) + return tonumber(GlobalsGetValue("fungal_shift_iteration", "0")) end -- Get the number of frames since the previous shift; -1 if none function get_last_shift_frame() - return tonumber(GlobalsGetValue("fungal_shift_last_frame", "-1")) + return tonumber(GlobalsGetValue("fungal_shift_last_frame", "-1")) end -- Get the pending cooldown in seconds; 0 if none or done function get_cooldown_sec() - local last_frame = get_last_shift_frame() - if last_frame == -1 then return 0 end + local last_frame = get_last_shift_frame() + if last_frame == -1 then return 0 end - local frame = GameGetFrameNum() - return (COOLDOWN - (frame - last_frame)) / 60 + local frame = GameGetFrameNum() + return (COOLDOWN - (frame - last_frame)) / 60 end --- Determine the numbered shift, where 0 is the first shift +--[[ Determine the numbered shift, where 0 is the first shift +-- +-- @param iter number +-- @returns table:{from=shift-info, to=shift-info} +--]] function sq_get_abs(iter) - local mat_from = matinfo.get_materials_from() - local mat_to = matinfo.get_materials_to() - SetRandomSeed(89346, 42345+iter) - local rnd = random_create(9123, 58925+iter) - - mat_from = pick_random_from_table_weighted(rnd, mat_from) - mat_to = pick_random_from_table_weighted(rnd, mat_to) - - mat_from.flask = false - mat_to.flask = false - if random_nexti(rnd, 1, 100) <= 75 then -- 75% to use a flask - if random_nexti(rnd, 1, 100) <= 50 then -- 50% which side gets it - mat_from.flask = true - else - mat_to.flask = true + local mats_from = matinfo.get_materials_from() + local mats_to = matinfo.get_materials_to() + SetRandomSeed(89346, 42345+iter) + local rnd = random_create(9123, 58925+iter) + + local mat_from = pick_random_from_table_weighted(rnd, mats_from) + local mat_to = pick_random_from_table_weighted(rnd, mats_to) + + mat_from.flask = false + mat_to.flask = false + if random_nexti(rnd, 1, 100) <= 75 then -- 75% to use a flask + if random_nexti(rnd, 1, 100) <= 50 then -- 50% which side gets it + mat_from.flask = true + else + mat_to.flask = true + end + end + + return {from=mat_from, to=mat_to} +end + +--[[ Determine if the shift uses a "rare" material +-- +-- @param shift_pair:table {from=shift-info, to=shift-info} +-- @returns rare_from:boolean, rare_to:boolean +--]] +function sq_is_rare_shift(shift_pair, cutoff) + if type(cutoff) ~= "number" then cutoff = CUTOFF_RARE end + local mats_from = shift_pair.from + local mat_to = shift_pair.to + local rare_from, rare_to = false, false + + for _, material in ipairs(mats_from) do + if matinfo.is_rare_source(material, cutoff) then + rare_from = true + end + end + + if matinfo.is_rare_target(mat_to.material, cutoff) then + rare_to = true end - end - return {from=mat_from, to=mat_to} + return rare_from, rare_to end --- vim: set ts=2 sts=2 sw=2: +-- vim: set ts=4 sts=4 sw=4: diff --git a/init.lua b/init.lua index 6b8f8c3..cefcc19 100644 --- a/init.lua +++ b/init.lua @@ -19,28 +19,29 @@ -- -- PLANNED FEATURES -- --- There's no ImGui fallback behavior, nor are there any diagnostic --- messages if ImGui isn't available. +-- Add either flask or pouch icon next to each material with the +-- material's CellData color (parse "data/materials.xml"). -- -- Add "fungal_shift_ui_icon" to the ImGui window. dofile_once("mods/shift_query/files/common.lua") -dofile_once("mods/shift_query/files/materials.lua") dofile_once("mods/shift_query/files/query.lua") dofile_once("mods/shift_query/lib/feedback.lua") dofile_once("mods/shift_query/files/constants.lua") APLC = dofile_once("mods/shift_query/files/aplc.lua") +--nxml = dofile_once("mods/shift_query/lib/nxml.lua") MAT_AP = "midas_precursor" MAT_LC = "magic_liquid_hp_regeneration_unstable" +RARE_MAT_COLOR = Feedback.colors.yellow_light + SQ = { new = function(self, imgui) self._imgui = imgui self._fb = Feedback:init(imgui) self._iter_track = -1 -- used for update detection self._frame_track = -1 -- used for update detection - self._force_update = false return self end, @@ -71,15 +72,10 @@ SQ = { idx_end = math.min(curr_iter + range_next, MAX_SHIFTS) end - q_logf("start = %s, end = %s", idx_start, idx_end) + q_logf("start=%s, end=%s", idx_start, idx_end) return {first=idx_start, last=idx_end} end, - --[[ Format the final shift line ]] - format_final = function(self, which, source, dest) - return ("%s shift is %s -> %s"):format(which, source, dest) - end, - --[[ Display either an AP or an LC recipe ]] print_aplc = function(self, mat, prob, combo) local result = maybe_localize_material(mat) @@ -100,14 +96,25 @@ SQ = { --[[ Determine and format a single shift ]] query = function(self, index) - q_logf("query(%s)", index) + q_logf("query(%d)", index) local iter = get_curr_iter() local shift = sq_get_abs(index) local which_msg = format_relative(iter, index) - for _, pair in ipairs(format_shift(shift)) do - local line = self:format_final(which_msg, pair[1], pair[2]) - q_log(line) - self._fb:add(line) + for idx, pair in ipairs(format_shift(shift)) do + q_logf("pair[%d]={%q, %q}", idx, pair[1], pair[2]) + -- To disable colors (for rare shift annotation), comment out the + -- code below and un-comment the following line. + --self._fb:add(("%s shift is %s -> %s"):format(which_msg, pair[1], pair[2])) + local rare_from, rare_to = sq_is_rare_shift(shift, nil) + local str_from = pair[1] + local str_to = pair[2] + if rare_from then + str_from = {color=RARE_MAT_COLOR, pair[1]} + end + if rare_to then + str_to = {color=RARE_MAT_COLOR, pair[2]} + end + self._fb:add({which_msg, "shift is", str_from, "->", str_to}) end end, @@ -143,8 +150,8 @@ SQ = { --[[ Determine if we should refresh the shift list ]] check_update = function(self) - if self._force_update then - self._force_update = false + if q_is_update_forced() then + q_clear_force_update() return true end local iter = get_curr_iter() @@ -163,35 +170,81 @@ SQ = { return draw end, + --[[ Obtain the actual list of shifted materials ]] + get_shift_map = function(self) + local world = EntityGetWithTag("world_state")[1] + local state = EntityGetComponent(world, "WorldStateComponent")[1] + local shifts = ComponentGetValue2(state, "changed_materials") + local shift_pairs = {} + for idx = 1, #shifts, 2 do + local mat1 = shifts[idx] + local mat2 = shifts[idx+1] + q_logf("shift %d shifted %s to %s", (idx+1)/2, mat1, mat2) + table.insert(shift_pairs, {mat1, mat2}) + end + return shift_pairs + end, + + --[[ Format the cooldown timer ]] + format_cooldown = function(self) + local last_shift_frame = get_last_shift_frame() + local cooldown = get_cooldown_sec() + if last_shift_frame > -1 then + if cooldown > 0 then + return format_duration(cooldown) + end + end + return nil + end, + --[[ Draw the menu bar ]] draw_menu = function(self) if self._imgui.BeginMenuBar() then if self._imgui.BeginMenu("Actions") then + if self._imgui.MenuItem("Refresh") then + self:refresh() + end + self._imgui.Separator() + local i18n_conf = q_setting_get(SETTING_LOCALIZE) if i18n_conf ~= FORMAT_LOCALE then if self._imgui.MenuItem("Show Local Names") then q_setting_set(SETTING_LOCALIZE, FORMAT_LOCALE) - self._force_update = true end end if i18n_conf ~= FORMAT_INTERNAL then if self._imgui.MenuItem("Show Internal Names") then q_setting_set(SETTING_LOCALIZE, FORMAT_INTERNAL) - self._force_update = true end end if i18n_conf ~= FORMAT_BOTH then if self._imgui.MenuItem("Show Local & Internal Names") then q_setting_set(SETTING_LOCALIZE, FORMAT_BOTH) - self._force_update = true end end + self._imgui.Separator() + local expand_opt = q_setting_get(SETTING_EXPAND) + if expand_opt == EXPAND_ONE then + if self._imgui.MenuItem("Show All Source Materials") then + q_setting_set(SETTING_EXPAND, EXPAND_ALL) + end + else + if self._imgui.MenuItem("Show Primary Source Material") then + q_setting_set(SETTING_EXPAND, EXPAND_ONE) + end + end + + self._imgui.Separator() + local real_str = f_enable(not q_setting_get(SETTING_REAL)) + if self._imgui.MenuItem(real_str .. " Flask Resolving") then + q_setting_set(SETTING_REAL, not q_setting_get(SETTING_REAL)) + end + self._imgui.Separator() local aplc_str = f_enable(not q_setting_get(SETTING_APLC)) if self._imgui.MenuItem(aplc_str .. " AP/LC Recipes") then q_setting_set(SETTING_APLC, not q_setting_get(SETTING_APLC)) - self._force_update = true end self._imgui.Separator() @@ -203,6 +256,7 @@ SQ = { self._fb:clear() end if self._imgui.MenuItem("Close") then + GamePrint("UI closed; re-open using the Mod Settings window") q_disable_gui() end self._imgui.EndMenu() @@ -212,25 +266,22 @@ SQ = { if self._imgui.BeginMenu("Prior Shifts") then if self._imgui.MenuItem("Show All") then q_setting_set(SETTING_PREVIOUS, tostring(ALL_SHIFTS)) - self._force_update = true end if self._imgui.MenuItem("Show One") then q_setting_set(SETTING_PREVIOUS, tostring(1)) - self._force_update = true end self._imgui.EndMenu() end if self._imgui.BeginMenu("Pending Shifts") then if self._imgui.MenuItem("Show All") then q_setting_set(SETTING_NEXT, tostring(ALL_SHIFTS)) - self._force_update = true end if self._imgui.MenuItem("Show Next") then q_setting_set(SETTING_NEXT, tostring(1)) - self._force_update = true end self._imgui.EndMenu() end + self._imgui.EndMenu() end self._imgui.EndMenuBar() @@ -239,39 +290,23 @@ SQ = { --[[ Draw the main window ]] draw_window = function(self) - self:_draw_window_main() - end, - - --[[ Draw the main window content ]] - _draw_window_main = function(self) local iter = get_curr_iter() - -- Draw a helpful "refresh now" button - if self._imgui.Button("Refresh Shifts") then - self:refresh() - end - if self:check_update() then self:refresh() end - -- Draw the feedback window Clear button - self._imgui.SameLine() - self._fb:draw_button() - -- Draw the current shift iteration + self._imgui.Text(("Shift: %s;"):format(iter)) + self._imgui.SameLine() - self._imgui.Text(("Shift: %s"):format(iter)) -- Draw the current shift cooldown - local last_shift_frame = get_last_shift_frame() - local cooldown = get_cooldown_sec() - if last_shift_frame > -1 then - if cooldown > 0 then - self._imgui.Text(("Cooldown: %s"):format(format_duration(cooldown))) - else - self._imgui.Text("Cooldown finished") - end + local cooldown = self:format_cooldown() + if cooldown ~= nil then + self._imgui.Text(("Cooldown: %s"):format(cooldown)) + else + self._imgui.Text("Cooldown finished") end -- Display what shifts the user has requested @@ -281,6 +316,18 @@ SQ = { local prev_text = f_shift_count(prev_c, "previous") self._imgui.Text(("Displaying %s and %s"):format(prev_text, next_text)) + if q_setting_get(SETTING_REAL) then + for _, matpair in ipairs(self:get_shift_map()) do + -- Don't localize the material; shifting affects that + local from_str, to_str = matpair[1], matpair[2] + self._fb:draw_line({ + {color="green", from_str}, + "became", + {color="green", to_str} + }) + end + end + self._fb:draw_box() end, @@ -291,16 +338,16 @@ SQ = { end } --- function OnWorldInitialized() end --- function OnModPostInit() end --- function OnPlayerSpawned(player_entity) end - imgui = nil query = nil +-- function OnWorldInitialized() end + function OnModPostInit() - imgui = load_imgui({version="1.3.0", mod="FungalShiftQuery"}) - query = SQ:new(imgui) + if load_imgui then + imgui = load_imgui({version="1.3.0", mod="FungalShiftQuery"}) + query = SQ:new(imgui) + end -- Fix problem with contradicting localize options (boolean / string) local localize = q_setting_get(SETTING_LOCALIZE) @@ -316,22 +363,34 @@ end -- The actual driving code, executed once per frame after world update function OnWorldPostUpdate() local ready = q_get_enabled() - local window_flags = imgui.WindowFlags.NoFocusOnAppearing - window_flags = window_flags + imgui.WindowFlags.MenuBar - window_flags = window_flags + imgui.WindowFlags.NoNavInputs if not imgui then - GamePrint("imgui not initialized") + GamePrint(table.concat({ + "shift_query - Noita-Dear-ImGui not found;", + "see workshop page for instructions"}, " ")) + GamePrint(table.concat({ + "shift_query - Ensure unsafe mods are enabled,", + "Noita-Dear-ImGui is installed and active,", + "and this mod is below Noita-Dear-ImGui in the mod list"}, " ")) ready = false - end - - if not query then - GamePrint("query object not initialized") + elseif not query then + GamePrint("shift_query - query object not initialized") ready = false end if ready then - if imgui.Begin("Fungal Shifts", nil, window_flags) then + local window_flags = bit.bor( + imgui.WindowFlags.NoFocusOnAppearing, + imgui.WindowFlags.MenuBar, + imgui.WindowFlags.NoNavInputs, + imgui.WindowFlags.HorizontalScrollbar) + local wtitle = "Fungal Shifts" + local last_shift = get_last_shift_frame() + local cooldown = get_cooldown_sec() + if last_shift > -1 and cooldown > 0 then + wtitle = ("%s (%s)"):format(wtitle, format_duration(cooldown)) + end + if imgui.Begin(wtitle .. "###Main", nil, window_flags) then local res, ret = pcall(query.draw, query) if not res then GamePrint(tostring(ret)) end imgui.End() diff --git a/lib/feedback.lua b/lib/feedback.lua index d6ca79b..5037b07 100644 --- a/lib/feedback.lua +++ b/lib/feedback.lua @@ -33,6 +33,27 @@ Feedback = { + colors = { + -- Pure colors + red = {1, 0, 0}, + green = {0, 1, 0}, + blue = {0, 0, 1}, + cyan = {0, 1, 1}, + magenta = {1, 0, 1}, + yellow = {1, 1, 0}, + white = {1, 1, 1}, + black = {0, 0, 0}, + + -- Blended colors + red_light = {1, 0.5, 0.5}, + green_light = {0.5, 1, 0.5}, + blue_light = {0.5, 0.5, 1}, + cyan_light = {0.5, 1, 1}, + magenta_light = {1, 0.5, 1}, + yellow_light = {1, 1, 0.5}, + gray = {0.5, 0.5, 0.5}, + }, + -- The lines table, public for convenience lines = {}, @@ -79,14 +100,48 @@ Feedback = { end end, + -- Convert a given color to a real color + get_color = function(self, color) + if color == nil then return nil end + if type(color) == "string" then + if self.colors[color] then + return self.colors[color] + end + end + if type(color) == "table" then + return { + color[1] or 0, + color[2] or 0, + color[3] or 0 + } + end + end, + + -- Draw a single line; public for convenience + draw_line = function(self, line) + if type(line) == "string" then + self._imgui.Text(line) + elseif type(line) == "table" then + local color = self:get_color(line.color) + if color then + self._imgui.PushStyleColor(self._imgui.Col.Text, unpack(color)) + end + for idx, part in ipairs(line) do + if idx > 1 then self._imgui.SameLine() end + self:draw_line(part) + end + if color then + self._imgui.PopStyleColor() + end + else + self._imgui.Text(tostring(line)) + end + end, + -- Draw the box of text draw_box = function(self) for _, line in ipairs(self.lines) do - if type(line) == "string" then - self._imgui.Text(line) - elseif type(line) == "table" then - self._imgui.Text(line[1]) -- TODO: font - end + self:draw_line(line) end end, diff --git a/lib/nxml.lua b/lib/nxml.lua new file mode 100644 index 0000000..6e150a5 --- /dev/null +++ b/lib/nxml.lua @@ -0,0 +1,742 @@ +---@alias int integer +---@alias bool boolean +---@alias str string +---@alias token_type "string" | "<" | ">" | "/" | "=" +---@alias error_type "missing_attribute_value" | "missing_element_close" | "missing_equals_sign" | "missing_element_name" | "missing_tag_open" | "mismatched_closing_tag" | "missing_token" | "missing_element" +---@alias error_fn fun(type: error_type, msg: str) + +---@class (exact) token +---@field value string? +---@field type token_type + +---@class (exact) error +---@field type error_type +---@field msg str +---@field row int +---@field col int + +---@class (exact) tokenizer +---@field data str +---@field cur_idx int +---@field cur_row int +---@field cur_col int +---@field prev_row int +---@field prev_col int +---@field len int + +---@class (exact) parser +---@field tok tokenizer_blob +---@field errors error[] +---@field error_reporter error_fn + +---@alias parser_blob parser_funcs | parser +---@alias tokenizer_blob tokenizer_funcs | tokenizer +---@alias element_blob element_funcs | element + +---@class (exact) element +---@field content str[]? +---@field children element_blob[] +---@field attr table +---@field name str +---@field errors error[] + +local ffi = nil +if require then + pcall(function() + ffi = require("ffi") + end) +end + +---@param str str +---@param start_idx int +---@param len int +---@return str +local function str_sub(str, start_idx, len) + return str:sub(start_idx + 1, start_idx + len) +end + +---@param str str +---@param idx int +---@return integer +local function str_index(str, idx) + return string.byte(str:sub(idx + 1, idx + 1)) +end + +--[[ + * The following is a Lua port of the NXML parser: + * https://github.com/xwitchproject/nxml + * + * The NXML Parser is heavily based on code from poro + * https://github.com/gummikana/poro + * + * The poro project is licensed under the Zlib license: + * + * -------------------------------------------------------------------------- + * Copyright (c) 2010-2019 Petri Purho, Dennis Belfrage + * Contributors: Martin Jonasson, Olli Harjola + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + * -------------------------------------------------------------------------- +]] + +---@class nxml +local nxml = {} + +---@class tokenizer_funcs +local TOKENIZER_FUNCS = {} +local TOKENIZER_MT = { + __index = TOKENIZER_FUNCS, + __tostring = function(self) return "natif.nxml.tokenizer" end +} + +---@param cstring str +---@param len int +---@return tokenizer_blob +local function new_tokenizer(cstring, len) + ---@type tokenizer + local tokenizer = { + data = cstring, + cur_idx = 0, + cur_row = 1, + cur_col = 1, + prev_row = 1, + prev_col = 1, + len = len + } + return setmetatable(tokenizer, TOKENIZER_MT) +end + +---@type table +local ws = { + [string.byte(" ")] = true, + [string.byte("\t")] = true, + [string.byte("\n")] = true, + [string.byte("\r")] = true +} + +---@param char int +---@return bool +function TOKENIZER_FUNCS:is_whitespace(char) + local n = tonumber(char) + return ws[n] or false +end + +---@type table +local punct = { + [string.byte("<")] = true, + [string.byte(">")] = true, + [string.byte("=")] = true, + [string.byte("/")] = true, +} + +---@param char int +---@return bool +function TOKENIZER_FUNCS:is_whitespace_or_punctuation(char) + local n = tonumber(char) + --- We can disable here because is_whitespace(!int) -> false + ---@diagnostic disable-next-line: param-type-mismatch + return self:is_whitespace(n) or punct[n] or false +end + +---@param n int? 1 +function TOKENIZER_FUNCS:move(n) + ---@cast self tokenizer_blob + n = n or 1 + local prev_idx = self.cur_idx + self.cur_idx = self.cur_idx + n + if self.cur_idx >= self.len then + self.cur_idx = self.len + return + end + for i = prev_idx, self.cur_idx - 1 do + if str_index(self.data, i) == string.byte("\n") then + self.cur_row = self.cur_row + 1 + self.cur_col = 1 + else + self.cur_col = self.cur_col + 1 + end + end +end + +---@param n int? 1 +---@return int +function TOKENIZER_FUNCS:peek(n) + ---@cast self tokenizer_blob + n = n or 1 + local idx = self.cur_idx + n + if idx >= self.len then return 0 end + + return str_index(self.data, idx) +end + +---@param str str +---@return boolean +function TOKENIZER_FUNCS:match_string(str) + local len = #str + + for i = 0, len - 1 do + if self:peek(i) ~= str_index(str, i) then return false end + end + return true +end + +---@return bool +function TOKENIZER_FUNCS:eof() + ---@cast self tokenizer_blob + return self.cur_idx >= self.len +end + +---@return int +function TOKENIZER_FUNCS:cur_char() + ---@cast self tokenizer_blob + if self:eof() then return 0 end + return str_index(self.data, self.cur_idx) +end + +function TOKENIZER_FUNCS:skip_whitespace() + while not self:eof() do + if self:is_whitespace(self:cur_char()) then + self:move() + elseif self:match_string("") do + self:move() + end + + if self:match_string("-->") then + self:move(3) + end + elseif self:cur_char() == string.byte("<") and self:peek(1) == string.byte("!") then + self:move(2) + while not self:eof() and self:cur_char() ~= string.byte(">") do + self:move() + end + if self:cur_char() == string.byte(">") then + self:move() + end + elseif self:match_string("") do + self:move() + end + if self:match_string("?>") then + self:move(2) + end + else + break + end + end +end + +---@return str +function TOKENIZER_FUNCS:read_quoted_string() + ---@cast self tokenizer_blob + local start_idx = self.cur_idx + local len = 0 + + while not self:eof() and self:cur_char() ~= string.byte("\"") do + len = len + 1 + self:move() + end + + self:move() -- skip " + return str_sub(self.data, start_idx, len) +end + +---@return str +function TOKENIZER_FUNCS:read_unquoted_string() + ---@cast self tokenizer_blob + local start_idx = self.cur_idx - 1 -- first char is move()d + local len = 1 + + while not self:eof() and not self:is_whitespace_or_punctuation(self:cur_char()) do + len = len + 1 + self:move() + end + + return str_sub(self.data, start_idx, len) +end + +local C_NULL = 0 +local C_LT = string.byte("<") +local C_GT = string.byte(">") +local C_SLASH = string.byte("/") +local C_EQ = string.byte("=") +local C_QUOTE = string.byte("\"") + +---@return token? +function TOKENIZER_FUNCS:next_token() + self:skip_whitespace() + + self.prev_row = self.cur_row + self.prev_col = self.cur_col + + if self:eof() then return nil end + + local c = self:cur_char() + self:move() + + if c == C_NULL then + return nil + elseif c == C_LT then + ---@type token + local v = { type = "<" } + return v + elseif c == C_GT then + ---@type token + local v = { type = ">" } + return v + elseif c == C_SLASH then + ---@type token + local v = { type = "/" } + return v + elseif c == C_EQ then + ---@type token + local v = { type = "=" } + return v + elseif c == C_QUOTE then + ---@type token + local v = { type = "string", value = self:read_quoted_string() } + return v + else + ---@type token + local v = { type = "string", value = self:read_unquoted_string() } + return v + end +end + +---@class parser_funcs +local PARSER_FUNCS = {} +local PARSER_MT = { + __index = PARSER_FUNCS, + __tostring = function(self) return "natif.nxml.parser" end +} + +---@param tokenizer tokenizer_blob +---@param error_reporter fun(type, msg)? +---@return parser | parser_funcs parser +local function new_parser(tokenizer, error_reporter) + ---@type parser + local parser = { + tok = tokenizer, + errors = {}, + error_reporter = error_reporter or function(type, msg) print("parser error: [" .. type .. "] " .. msg) end + } + return setmetatable(parser, PARSER_MT) +end + +---@class element_funcs +local XML_ELEMENT_FUNCS = {} +local XML_ELEMENT_MT = { + __index = XML_ELEMENT_FUNCS, + __tostring = function(self) + return nxml.tostring(self, false) + end, +} + +---@param type error_type +---@param msg str +function PARSER_FUNCS:report_error(type, msg) + ---@cast self parser_blob + self.error_reporter(type, msg) + ---@type error + local error = { type = type, msg = msg, row = self.tok.prev_row, col = self.tok.prev_col } + table.insert(self.errors, error) +end + +---@param attr_table table +---@param name str +function PARSER_FUNCS:parse_attr(attr_table, name) + ---@cast self parser_blob + local tok = self.tok:next_token() + if not tok then + self:report_error("missing_token", string.format("parsing attribute '%s' - did not find a token", name)) + return + end + if tok.type == "=" then + tok = self.tok:next_token() + + if not tok then + self:report_error("missing_token", string.format("parsing attribute '%s' - did not find a token", name)) + return + end + + if tok.type == "string" then + attr_table[name] = tok.value + else + self:report_error("missing_attribute_value", + string.format("parsing attribute '%s' - expected a string after =, but did not find one", name)) + end + else + self:report_error("missing_equals_sign", + string.format("parsing attribute '%s' - did not find equals sign after attribute name", name)) + end +end + +---@param skip_opening_tag bool +---@return element_blob? +function PARSER_FUNCS:parse_element(skip_opening_tag) + ---@cast self parser_blob + local tok + if not skip_opening_tag then + tok = self.tok:next_token() + if not tok then + self:report_error("missing_token", "parsing element - did not find a token") + return + end + if tok.type ~= "<" then + self:report_error("missing_tag_open", "couldn't find a '<' to start parsing with") + end + end + + tok = self.tok:next_token() + if not tok then + self:report_error("missing_token", "parsing element - did not find a token") + return + end + if tok.type ~= "string" then + self:report_error("missing_element_name", "expected an element name after '<'") + end + + local elem_name = tok.value + if not elem_name then + self:report_error("missing_attribute_value", "parse element element missing name") + return + end + local elem = nxml.new_element(elem_name) + local content_idx = 0 + + local self_closing = false + + while true do + tok = self.tok:next_token() + + if tok == nil then + return elem + elseif tok.type == "/" then + if self.tok:cur_char() == C_GT then + self.tok:move() + self_closing = true + end + break + elseif tok.type == ">" then + break + elseif tok.type == "string" then + self:parse_attr(elem.attr, tok.value) + end + end + + if self_closing then return elem end + + while true do + tok = self.tok:next_token() + + if tok == nil then + return elem + elseif tok.type == "<" then + if self.tok:cur_char() == C_SLASH then + self.tok:move() + + local end_name = self.tok:next_token() + if not end_name then + self:report_error("missing_token", + string.format("parsing element '%s' - did not find a token", elem_name)) + return + end + if end_name.type == "string" and end_name.value == elem_name then + local close_greater = self.tok:next_token() + if not close_greater then + self:report_error("missing_token", + string.format("parsing element '%s' - did not find a token", elem_name)) + return + end + + if close_greater.type == ">" then + return elem + else + self:report_error("missing_element_close", + string.format("no closing '>' found for element '%s'", elem_name)) + end + else + self:report_error("mismatched_closing_tag", + string.format("closing element is in wrong order - expected '', but instead got '%s'", + elem_name, tostring(end_name.value))) + end + return elem + else + local child = self:parse_element(true) + table.insert(elem.children, child) + end + else + if not elem.content then + elem.content = {} + end + + content_idx = content_idx + 1 + elem.content[content_idx] = tok.value or tok.type + end + end +end + +---@return element_blob[] +function PARSER_FUNCS:parse_elements() + ---@cast self parser_blob + local tok = self.tok:next_token() + ---@type element_blob[] + local elems = {} + local elems_i = 1 + + while tok and tok.type == "<" do + local next_element = self:parse_element(true) + if not next_element then + self.error_reporter("missing_element", "parse_element returned nil while parsing elements") + return elems + end + elems[elems_i] = next_element + elems_i = elems_i + 1 + + tok = self.tok:next_token() + end + + return elems +end + +---@param str str +---@return bool +local function is_punctuation(str) + return str == "/" or str == "<" or str == ">" or str == "=" +end + +---@return str +function XML_ELEMENT_FUNCS:text() + ---@cast self element_blob + if self.content == nil then return "" end + local content_count = #self.content + if content_count == 0 then return "" end + + local text = self.content[1] + for i = 2, content_count do + local elem = self.content[i] + local prev = self.content[i - 1] + + if is_punctuation(elem) or is_punctuation(prev) then + text = text .. elem + else + text = text .. " " .. elem + end + end + + return text +end + +---@param child element_blob +function XML_ELEMENT_FUNCS:add_child(child) + self.children[#self.children + 1] = child +end + +---@param children element_blob[] +function XML_ELEMENT_FUNCS:add_children(children) + local children_i = #self.children + 1 + for i = 1, #children do + self.children[children_i] = children[i] + children_i = children_i + 1 + end +end + +---@param child element_blob +function XML_ELEMENT_FUNCS:remove_child(child) + for i = 1, #self.children do + if self.children[i] == child then + table.remove(self.children, i) + break + end + end +end + +---@param index int +function XML_ELEMENT_FUNCS:remove_child_at(index) + table.remove(self.children, index) +end + +function XML_ELEMENT_FUNCS:clear_children() + ---@cast self element_blob + self.children = {} +end + +function XML_ELEMENT_FUNCS:clear_attrs() + self.attr = {} +end + +---@param element_name str +---@return element_blob? +function XML_ELEMENT_FUNCS:first_of(element_name) + local i = 0 + local n = #self.children + + while i < n do + i = i + 1 + local c = self.children[i] + + if c.name == element_name then return c end + end + + return nil +end + +---@param element_name str +---@return fun(): element_blob? +function XML_ELEMENT_FUNCS:each_of(element_name) + local i = 1 + local n = #self.children + + return function() + while i <= n do + local child = self.children[i] + i = i + 1 + if child.name == element_name then + return child + end + end + end +end + +---@param element_name str +---@return element_blob[] +function XML_ELEMENT_FUNCS:all_of(element_name) + local table = {} + local i = 1 + for elem in self:each_of(element_name) do + table[i] = elem + i = i + 1 + end + return table +end + +---@return fun(): element_blob +function XML_ELEMENT_FUNCS:each_child() + local i = 0 + local n = #self.children + + return function() + while i < n do + i = i + 1 + return self.children[i] + end + end +end + +---@param data str +---@return element_blob +function nxml.parse(data) + local data_len = #data + local tok = new_tokenizer(data, data_len) + local parser = new_parser(tok) + + local elem = parser:parse_element(false) + + if not elem or (elem.errors and #elem.errors > 0) then + error("parser encountered errors") + end + + return elem +end + +---@param data str +---@return element_blob[] +function nxml.parse_many(data) + local data_len = #data + local tok = new_tokenizer(data, data_len) + local parser = new_parser(tok) + + local elems = parser:parse_elements() + + for i = 1, #elems do + local elem = elems[i] + + if elem.errors and #elem.errors > 0 then + error("parser encountered errors") + end + end + + return elems +end + +---@param name str +---@param attrs table? {} +---@return element_blob +function nxml.new_element(name, attrs) + ---@type element + local element = { + name = name, + attr = attrs or {}, + children = {}, + errors = {}, + content = nil + } + return setmetatable(element, XML_ELEMENT_MT) +end + +---@param value str | bool +---@return str +local function attr_value_to_str(value) + local t = type(value) + if t == "string" then return value end + if t == "boolean" then return value and "1" or "0" end + + return tostring(value) +end + +---@param elem element_blob +---@param packed bool +---@param indent_char str? \t +---@param cur_indent str? "" +---@return str +function nxml.tostring(elem, packed, indent_char, cur_indent) + indent_char = indent_char or "\t" + cur_indent = cur_indent or "" + local s = "<" .. elem.name + local self_closing = #elem.children == 0 and (not elem.content or #elem.content == 0) + + for k, v in pairs(elem.attr) do + s = s .. " " .. k .. "=\"" .. attr_value_to_str(v) .. "\"" + end + + if self_closing then + s = s .. " />" + return s + end + + s = s .. ">" + + local deeper_indent = cur_indent .. indent_char + + if elem.content and #elem.content ~= 0 then + if not packed then s = s .. "\n" .. deeper_indent end + s = s .. elem:text() + end + + if not packed then s = s .. "\n" end + + for i, v in ipairs(elem.children) do + if not packed then s = s .. deeper_indent end + s = s .. nxml.tostring(v, packed, indent_char, deeper_indent) + if not packed then s = s .. "\n" end + end + + s = s .. cur_indent .. "" + + return s +end + +return nxml diff --git a/mod_description.txt b/mod_description.txt new file mode 100644 index 0000000..c686da8 --- /dev/null +++ b/mod_description.txt @@ -0,0 +1,18 @@ +What are the fungal shifts for this seed? Which shifts will use a flask? + +This mod will tell you exactly these. It lists all pending shifts (configurable) using either the internal material name or your localized name (also configurable). + +If a shift mentions using a flask, then a flask will be used, assuming you're holding one. If a flask isn't mentioned, then the shift won't use one. + +**NEW!** Alchemic precursor and lively concoction are now included! The number next to the materials is the conversion chance. A higher number means a more effective conversion. +**NEW!** Flask resolving! If enabled via the Actions menu, this mod will tell you exactly what materials have been shifted, even if a flask was used. +**NEW!** Live feedback for shift delay period! This mod will tell you exactly how many seconds remain before you can perform another fungal shift. + +Alchemic Precursor / Lively Concoction behavior: These recipes are randomly generated every seed and take three random materials to generate Alchemic Precursor or Lively Concoction. The behavior is as follows: +Material_2, in the presence of Material_1 and Material_3, has a N percent chance to become Alchemic Precursor or Lively Concoction. Therefore, you want a large amount of the second material and a small amount of the first and third materials. A higher conversion percentage results in a more efficient conversion. + +This mod requires Noita-Dear-ImGui, which requires unsafe mods to be enabled. + +This has been tested against the February 14th, 2024 update. Note that the Beta (as of March 3rd, 2024) includes more materials and so this mod may give you incorrect results. + +Feedback welcome! diff --git a/mod_id.txt b/mod_id.txt index 86cfe31..b515e30 100644 --- a/mod_id.txt +++ b/mod_id.txt @@ -1 +1 @@ -shift_query +shift_query \ No newline at end of file diff --git a/settings.lua b/settings.lua index a1fbd41..d82b79d 100644 --- a/settings.lua +++ b/settings.lua @@ -1,72 +1,87 @@ +--[[ -- Configuration script for the Fungal Shift Query mod -- --- PLANNED FEATURES +-- Changing any setting (other than enable) triggers a force update. -- --- ** Ability to add/remove materials to existing source shift sets (both --- one-at-a-time and in bulk) --- ** Ability to modify source and destination probabilities --- ** Ability to add/remove source and destination shift sets (both --- one-at-a-time and in bulk) --- ** Ability to override MAX_SHIFTS - ---[[ --- FIXME: previous_count and next_count display -2 .. 20 --- FIXME: previous_count and next_count are not integers +-- Note that this script cannot reference any file in the mods/ directory, as +-- the virtual filesystem is not yet initialized by the time this script runs. +-- Therefore, all values in files/common.lua and files/constants.lua are +-- instead hard-coded here. --]] dofile_once("data/scripts/lib/utilities.lua") dofile_once("data/scripts/lib/mod_settings.lua") -dofile_once("mods/shift_query/files/common.lua") +MOD_ID = "shift_query" +MIN_SHIFTS = -1 +MAX_SHIFTS = 20 --- luacheck: globals MOD_SETTING_SCOPE_RUNTIME +function sq_mod_shift_range(mod_id, gui, in_main_menu, im_id, setting) + -- luacheck: globals ModSettingGetNextValue ModSettingSetNextValue + -- luacheck: globals GuiSlider mod_setting_group_x_offset + -- luacheck: globals mod_setting_handle_change_callback + -- luacheck: globals mod_setting_tooltip mod_setting_get_id + local value = ModSettingGetNextValue(mod_setting_get_id(mod_id, setting)) + if type(value) ~= "number" then value = setting.default or 0 end + + local value_new = GuiSlider( + gui, im_id, + mod_setting_group_x_offset, 0, + setting.ui_name, + value, + setting.value_min, + setting.value_max, + setting.value_default, + setting.value_display_multiplier or 1, + setting.value_display_formatting or "", 64) + --value_new = clamp(math.floor(value_new), MIN_SHIFTS, MAX_SHIFTS) + value_new = math.floor(value_new) + if value ~= value_new then + ModSettingSetNextValue(mod_setting_get_id(mod_id, setting), value_new, false) + mod_setting_handle_change_callback(mod_id, gui, in_main_menu, setting, value, value_new) + end --- Available functions: --- ModSettingSetNextValue(setting_id, next_value, true/false) --- ModSettingSet(setting_id, new_value) + mod_setting_tooltip(mod_id, gui, in_main_menu, setting) +end -function mod_setting_changed_callback(mod_id, gui, in_main_menu, setting, old_value, new_value) - --[[ TODO: enforce integer values - logger_add(("Setting %s changed from %s to %s"):format( - setting.id, tostring(old_value), tostring(new_value))) - if setting.id == "previous_count" or setting.id == "next_count" then - local final_value = math.floor(new_value) - if final_value < MIN_SHIFTS then - logger_add(("Setting %s %d below %d"):format(setting.id, new_value, final_value)) - final_value = MIN_SHIFTS - elseif final_value > MAX_SHIFTS then - logger_add(("Setting %s %d above %d"):format(setting.id, new_value, final_value)) - final_value = MAX_SHIFTS - end - if new_value ~= final_value then - ModSettingSet(MOD_ID .. "." .. setting.id, final_value) - end - return final_value +function sq_setting_changed( mod_id, gui, in_main_menu, setting, old_value, new_value ) + if old_value ~= new_value then + GlobalsSetValue("shift_query.q_force_update", "1") end - ]] end -mod_settings_version = 3 +mod_settings_version = 5 mod_settings = { + -- luacheck: globals MOD_SETTING_SCOPE_RUNTIME + { + id = "enable", + ui_name = "Enable UI", + ui_description = "Display GUI", + value_default = true, + change_fn = sq_setting_changed, + scope = MOD_SETTING_SCOPE_RUNTIME, + }, { id = "previous_count", ui_name = "Previous count", ui_description = "How many previous shifts should we display? (-1 = all)", value_default = 0, - value_min = MIN_SHIFTS, - value_max = MAX_SHIFTS, + value_min = -0.5, + value_max = 20, value_display_multiplier = 1, - change_fn = mod_setting_changed_callback, + change_fn = sq_setting_changed, + ui_fn = sq_mod_shift_range, scope = MOD_SETTING_SCOPE_RUNTIME, }, { id = "next_count", ui_name = "Next count", ui_description = "How many pending shifts should we display? (-1 = all)", - value_default = -1, - value_min = MIN_SHIFTS, - value_max = MAX_SHIFTS, + value_default = 20, + value_min = -0.5, + value_max = 20, value_display_multiplier = 1, - change_fn = mod_setting_changed_callback, + change_fn = sq_setting_changed, + ui_fn = sq_mod_shift_range, scope = MOD_SETTING_SCOPE_RUNTIME, }, { @@ -75,17 +90,23 @@ mod_settings = { ui_description = "How should material names be displayed?", value_default = "locale", values = { - {FORMAT_LOCALE, "Localized Name"}, - {FORMAT_INTERAL, "Internal Name"}, - {FORMAT_BOTH, "Both"} + {"locale", "Translated Name"}, + {"internal", "Internal Name"}, + {"both", "Both"}, }, + change_fn = sq_setting_changed, scope = MOD_SETTING_SCOPE_RUNTIME, }, { - id = "enable", - ui_name = "Enable UI", - ui_description = "Display GUI", - value_default = true, + id = "expand_from", + ui_name = "Expand sources?", + ui_description = "Show all source materials, or just the primary one?", + value_default = "one", + values = { + {"one", "Primary Material"}, + {"all", "All Materials"}, + }, + change_fn = sq_setting_changed, scope = MOD_SETTING_SCOPE_RUNTIME, }, { @@ -93,6 +114,15 @@ mod_settings = { ui_name = "Include AP / LC recipes", ui_description = "Include Alchemic Precursor and Lively Concoction recipes", value_default = false, + change_fn = sq_setting_changed, + scope = MOD_SETTING_SCOPE_RUNTIME, + }, + { + id = "flask_real", + ui_name = "Resolve Flasks", + ui_description = "Show a log of past shifts with flasks resolved to the real material", + value_default = false, + change_fn = sq_setting_changed, scope = MOD_SETTING_SCOPE_RUNTIME, }, } diff --git a/shift_query.vdf b/shift_query.vdf new file mode 100644 index 0000000..7c4d6e9 --- /dev/null +++ b/shift_query.vdf @@ -0,0 +1,10 @@ +"workshopitem" +{ + "appid" "881100" + "contentfolder" "/home/kaedenn/.local/share/Steam/steamapps/common/Noita/mods/shift_query" + "previewfile" "/home/kaedenn/.local/share/Steam/steamapps/common/Noita/mods/shift_query/workshop_preview_image.png" + "visibility" "0" // 0=public, 1=friends only, 2=private + "title" "Fungal Shift Query" + "description" "" + "publishedfileid" "3132525756" +}