diff options
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/boxdestroyer/boxdestroyer.lua')
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/boxdestroyer/boxdestroyer.lua | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/boxdestroyer/boxdestroyer.lua b/Data/BuiltIn/Libraries/lua-addons/addons/boxdestroyer/boxdestroyer.lua new file mode 100644 index 0000000..af70616 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/boxdestroyer/boxdestroyer.lua @@ -0,0 +1,466 @@ +--[[ +Copyright (c) 2014, Seth VanHeulen +All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--]] + +-- addon information + +_addon.name = 'boxdestroyer' +_addon.version = '1.0.5' +_addon.command = 'boxdestroyer' +_addon.author = 'Seth VanHeulen (Acacia@Odin)' + +-- modules + +require('pack') +require('tables') +require('chat') + +-- load message constants + +require('messages') + +-- config + +config = require('config') +defaults = { + HighlightResult = false, + HighlightColor = 36, +} +settings = config.load(defaults) +-- global constants + +default = { + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99 +} + +observed_default = { + ['second_even_odd'] = false, + ['first_even_odd'] = false, + ['range'] = false, + ['equal'] = false, + ['second_multiple'] = false, + ['first_multiple'] = false, + ['thief_tools_active'] = false +} + +thief_tools = {[1022] = true} + +-- global variables + +box = {} +observed = {} +zone_id = windower.ffxi.get_info().zone + +-- filter helper functions + +function greater_less(id, greater, num) + local new = {} + for _,v in pairs(box[id]) do + if greater and v > num or not greater and v < num then + table.insert(new, v) + end + end + return new +end + +function even_odd(id, div, rem) + local new = {} + for _,v in pairs(box[id]) do + if (math.floor(v / div) % 2) == rem then + table.insert(new, v) + end + end + return new +end + +function equal(id, first, num) + local new = {} + for _,v in pairs(box[id]) do + if first and math.floor(v / 10) == num or not first and (v % 10) == num then + table.insert(new, v) + end + end + return new +end + +function exclusive_mean(counts) + total = 0 + for _,v in pairs(counts) do + total = total + v + end + weighted_mean = 0 + for _,v in pairs(counts) do + weighted_mean = weighted_mean + (total - v) * v / total + end + return weighted_mean +end + +function calculate_odds(id,chances) + local reductions = {} + if not observed[id].first_even_odd then + local counter = {0} + counter[0] = 0 + for _,v in pairs(box[id]) do + counter[math.floor(v / 10) % 2] = counter[math.floor(v / 10) % 2] + 1 + end + reductions[#reductions+1] = exclusive_mean(counter) + end + if not observed[id].second_even_odd then + local counter = {0} + counter[0] = 0 + for _,v in pairs(box[id]) do + counter[v % 2] = counter[v % 2] + 1 + end + reductions[#reductions+1] = exclusive_mean(counter) + end + if not observed[id].range then + local new = {} + local reduction = 0 + for i,v in pairs(box[id]) do + new[i] = 0 + for _,m in pairs(box[id]) do + if m-v > 16 then break end + new[i] = new[i] + math.max(16-math.abs(m - v),0)^2/256 + end + reduction = reduction + (#box[id] - new[i])/#box[id] + end + + reductions[#reductions+1] = reduction + end + if not observed[id].equal then + local counter = {0,0,0,0,0,0,0,0,0} + counter[0] = 0 + local eliminated = {0,0,0,0,0,0,0,0,0} + eliminated[0] = 0 + for _,v in pairs(box[id]) do + counter[math.floor(v / 10)] = counter[math.floor(v / 10)] + 1/2 + counter[v % 10] = counter[v % 10] + 1/2 + for i = 0,9 do + if i ~= v % 10 and i ~= math.floor(v / 10) then + eliminated[i] = eliminated[i] + 1 + end + end + end + + reduction = 0 + for i,v in pairs(counter) do + reduction = reduction + eliminated[i] * v / #box[id] + end + + reductions[#reductions+1] = reduction + end + if not observed[id].second_multiple then + local counter = {0,0,0,0,0,0,0,0,0} + counter[0] = 0 + for _,v in pairs(box[id]) do + counter[v % 10] = counter[v % 10] + 1 + end + + local weights = { + counter[0] + counter[1]/2 + counter[2]/3, + counter[1]/2 + counter[2]/3 + counter[3]/3, + counter[2]/3 + counter[3]/3 + counter[4]/3, + counter[3]/3 + counter[4]/3 + counter[5]/3, + counter[4]/3 + counter[5]/3 + counter[6]/3, + counter[5]/3 + counter[6]/3 + counter[7]/3, + counter[6]/3 + counter[7]/3 + counter[8]/2, + counter[7]/3 + counter[8]/2 + counter[9] + } + + local eliminated = { + counter[3] + counter[4] + counter[5] + counter[6] + counter[7] + counter[8] + counter[9], + counter[0] + counter[4] + counter[5] + counter[6] + counter[7] + counter[8] + counter[9], + counter[0] + counter[1] + counter[5] + counter[6] + counter[7] + counter[8] + counter[9], + counter[0] + counter[1] + counter[2] + counter[6] + counter[7] + counter[8] + counter[9], + counter[0] + counter[1] + counter[2] + counter[3] + counter[7] + counter[8] + counter[9], + counter[0] + counter[1] + counter[2] + counter[3] + counter[4] + counter[8] + counter[9], + counter[0] + counter[1] + counter[2] + counter[3] + counter[4] + counter[5] + counter[9], + counter[0] + counter[1] + counter[2] + counter[3] + counter[4] + counter[5] + counter[6] + } + + local reduction = 0 + for i,v in pairs(weights) do + reduction = reduction + eliminated[i] * v / #box[id] + end + + reductions[#reductions + 1] = reduction + end + if not observed[id].first_multiple then + local counter = {0,0,0,0,0,0,0,0,0} + for _,v in pairs(box[id]) do + counter[math.floor(v / 10)] = counter[math.floor(v / 10)] + 1 + end + + local weights = { + counter[1] + counter[2]/2 + counter[3]/3, + counter[2]/2 + counter[3]/3 + counter[4]/3, + counter[3]/3 + counter[4]/3 + counter[5]/3, + counter[4]/3 + counter[5]/3 + counter[6]/3, + counter[5]/3 + counter[6]/3 + counter[7]/3, + counter[6]/3 + counter[7]/3 + counter[8]/2, + counter[7]/3 + counter[8]/2 + counter[9] + } + + local eliminated = { + counter[4] + counter[5] + counter[6] + counter[7] + counter[8] + counter[9], + counter[1] + counter[5] + counter[6] + counter[7] + counter[8] + counter[9], + counter[1] + counter[2] + counter[6] + counter[7] + counter[8] + counter[9], + counter[1] + counter[2] + counter[3] + counter[7] + counter[8] + counter[9], + counter[1] + counter[2] + counter[3] + counter[4] + counter[8] + counter[9], + counter[1] + counter[2] + counter[3] + counter[4] + counter[5] + counter[9], + counter[1] + counter[2] + counter[3] + counter[4] + counter[5] + counter[6] + } + + local reduction = 0 + for i,v in pairs(weights) do + reduction = reduction + eliminated[i] * v / #box[id] + end + + reductions[#reductions + 1] = reduction + end + + local expected_examine_value = 0 + for _,v in pairs(reductions) do + expected_examine_value = expected_examine_value + v/#reductions + end + + local optimal_guess = math.ceil(#box[id] / 2) + + local expected_guess_value = 2*optimal_guess - 2*optimal_guess^2 / #box[id] + 2*optimal_guess/#box[id] - 1 / #box[id] + + return expected_examine_value, expected_guess_value +end + +-- display helper function + +function display(id, chances) + if #box[id] == 90 then + windower.add_to_chat(207, 'Possible combinations: 10~99') + else + windower.add_to_chat(207, 'Possible combinations: ' .. table.concat(box[id], ' ')) + end + local remaining = math.floor(#box[id] / math.pow(2, (chances - 1))) + if remaining == 0 then + remaining = 1 + end + + if chances == 1 and observed[id].equal then + -- The "equal" message (== "X") for X in 1..9 gives an unequal probability to the remaining options + -- because "XX" is twice as likely to be indicated by the "equal" message. + -- This is too annoying to propagate to the rest of the addon, although it should be some day. + local printed = false + for _,v in pairs(box[id]) do + if math.floor(v/10) == v%10 then + windower.add_to_chat(207, 'Best guess: %d (%d%%)':format(v, 1 / remaining * 100)) + printed = true + break + end + end + if not printed then + windower.add_to_chat(207, 'Best guess: %d (%d%%)':format(box[id][math.ceil(#box[id] / 2)], 1 / remaining * 100)) + end + else + windower.add_to_chat(207, 'best guess: %d (%d%%)':format(box[id][math.ceil(#box[id] / 2)], 1 / remaining * 100)) + local clue_value,guess_value = calculate_odds(id,chances) + local result = clue_value > guess_value and remaining ~= 1 and 'examining the chest' or 'guessing ' .. '%d':format(box[id][math.ceil(#box[id] / 2)]) + local formatted_result = settings.HighlightResult and result:color(settings.HighlightColor) or result + windower.add_to_chat(207, 'boxdestroyer recommends ' .. formatted_result .. '.') + end + +end + +-- ID obtaining helper function +function get_id(zone_id,str) + return messages[zone_id] + offsets[str] +end + +-- event callback functions + +function check_incoming_chunk(id, original, modified, injected, blocked) + if id == 0x0A then + zone_id = original:unpack('H', 49) + elseif messages[zone_id] then + if id == 0x0B then + box = {} + observed = {} + elseif id == 0x2A then + local box_id = original:unpack('I', 5) + local param0 = original:unpack('I', 9) + local param1 = original:unpack('I', 13) + local param2 = original:unpack('I', 17) + local message_id = original:unpack('H', 27) % 0x8000 + + if box[box_id] == nil then + box[box_id] = table.copy(default) + end + if observed[box_id] == nil then + observed[box_id] = table.copy(observed_default) + end + + if get_id(zone_id,'greater_less') == message_id then + box[box_id] = greater_less(box_id, param1 == 0, param0) + elseif get_id(zone_id,'second_even_odd') == message_id then + -- tells whether the second digit is even or odd + box[box_id] = even_odd(box_id, 1, param0) + observed[box_id].second_even_odd = true + elseif get_id(zone_id,'first_even_odd') == message_id then + -- tells whether the first digit is even or odd + box[box_id] = even_odd(box_id, 10, param0) + observed[box_id].first_even_odd = true + elseif get_id(zone_id,'range') == message_id then + if observed[box_id].thief_tools_active then + -- Thief tools are the same as normal ranges but with larger bounds. + -- lower bound (param0) = solution - RANDINT(8,32) + -- upper bound (param1) = solution + RANDINT(8,32) + -- param0 + 33 > solution > param0 + 7 + -- param1 - 7 > solution > param1 - 33 + -- if the bound is less than 11 or greater than 98, the message changes to "greater" or "less" respectively + box[box_id] = greater_less(box_id, true, math.max(param1-33,param0+7) ) + box[box_id] = greater_less(box_id, false, math.min(param0+33,param1-7) ) + observed[box_id].thief_tools_active = false + else + -- lower bound (param0) = solution - RANDINT(5,20) + -- upper bound (param1) = solution + RANDINT(5,20) + -- param0 + 21 > solution > param0 + 4 + -- param1 - 4 > solution > param1 - 21 + -- if the bound is less than 11 or greater than 98, the message changes to "greater" or "less" respectively + box[box_id] = greater_less(box_id, true, math.max(param1-21,param0+4) ) + box[box_id] = greater_less(box_id, false, math.min(param0+21,param1-4) ) + observed[box_id].range = true + end + elseif get_id(zone_id,'less') == message_id then + -- Less is a range with 9 as the lower bound + if observed[box_id].thief_tools_active then + box[box_id] = greater_less(box_id, true, math.max(9, param0-33) ) + box[box_id] = greater_less(box_id, false, math.min(10+33,param0-7) ) + observed[box_id].thief_tools_active = false + else + box[box_id] = greater_less(box_id, true, math.max(9, param0-21) ) + box[box_id] = greater_less(box_id, false, math.min(10+21,param0-4) ) + observed[box_id].range = true + end + elseif get_id(zone_id,'greater') == message_id then + -- Greater is a range with 100 as the upper bound + if observed[box_id].thief_tools_active then + box[box_id] = greater_less(box_id, true, math.max(99-33,param0+7) ) + box[box_id] = greater_less(box_id, false, math.min(100,param0+33) ) + observed[box_id].thief_tools_active = false + else + box[box_id] = greater_less(box_id, true, math.max(99-21,param0+4) ) + box[box_id] = greater_less(box_id, false, math.min(100,param0+21) ) + observed[box_id].range = true + end + elseif get_id(zone_id,'equal') == message_id then + -- single number that is either the first or second digit of the solution + local new = equal(box_id, true, param0) + local duplicate = param0 * 10 + param0 + for k,v in pairs(new) do + if v == duplicate then + table.remove(new, k) + end + end + for _,v in pairs(equal(box_id, false, param0)) do table.insert(new, v) end + table.sort(new) + box[box_id] = new + observed[box_id].equal = true + elseif get_id(zone_id,'second_multiple') == message_id then + -- three digit range including the second digit of the solution + local new = equal(box_id, false, param0) + for _,v in pairs(equal(box_id, false, param1)) do table.insert(new, v) end + for _,v in pairs(equal(box_id, false, param2)) do table.insert(new, v) end + table.sort(new) + box[box_id] = new + observed[box_id].second_multiple = true + elseif get_id(zone_id,'first_multiple') == message_id then + -- three digit range including the first digit of the solution + local new = equal(box_id, true, param0) + for _,v in pairs(equal(box_id, true, param1)) do table.insert(new, v) end + for _,v in pairs(equal(box_id, true, param2)) do table.insert(new, v) end + table.sort(new) + box[box_id] = new + observed[box_id].first_multiple = true + elseif get_id(zone_id,'success') == message_id or get_id(zone_id,'failure') == message_id then + box[box_id] = nil + elseif get_id(zone_id,'tool_failure') == message_id then + observed[box_id].thief_tools_active = false + end + elseif id == 0x34 then + local box_id = original:unpack('I', 5) + if windower.ffxi.get_mob_by_id(box_id).name == 'Treasure Casket' then + local chances = original:byte(9) + if box[box_id] == nil then + box[box_id] = table.copy(default) + observed[box_id] = table.copy(observed_default) + end + if chances > 0 and chances < 7 then + display(box_id, chances) + end + end + elseif id == 0x5B then + box[original:unpack('I', 17)] = nil + observed[original:unpack('I', 17)] = nil + end + end +end + +function watch_for_keys(id, original, modified, injected, blocked) + if blocked then + elseif (id == 0x036 or id == 0x037) and + windower.ffxi.get_mob_by_id(modified:unpack('I',0x05)).name == 'Treasure Casket' and + (windower.ffxi.get_player().main_job == 'THF' or windower.ffxi.get_player().sub_job == 'THF') then + + local box_id = modified:unpack('I',0x05) + if not box[box_id] then + box[box_id] = table.copy(default) + observed[box_id] = table.copy(observed_default) + end + + if id == 0x037 and thief_tools[windower.ffxi.get_items(modified:byte(0x11))[modified:byte(0xF)].id] then + observed[box_id].thief_tools_active = true + elseif id == 0x036 then + for i = 1,9 do + if thief_tools[windower.ffxi.get_items(0)[modified:byte(0x30+i)].id] then + observed[box_id].thief_tools_active = true + break + end + end + end + end +end + +-- register event callbacks + +windower.register_event('incoming chunk', check_incoming_chunk) + +windower.register_event('outgoing chunk', watch_for_keys) |