ah_sce_unpacked/unpacked/Custom_Tile New Deck Importer a28140.ttslua
2021-09-30 19:49:38 -04:00

570 lines
18 KiB
Plaintext

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Whimsical.
--- DateTime: 2021-08-19 6:38 a.m.
---
---@type ArkhamImportConfiguration
local tags = { configuration = "import_configuration_provider" }
local Priority = {
ERROR = 0,
WARNING = 1,
INFO = 2,
DEBUG = 3
}
---@type fun(text: string)
local print_fun = print
local print_priority = Priority.DEBUG
---@param priority number
---@return string
function Priority.get_label(priority)
if priority==0 then return "ERROR"
elseif priority==1 then return "WARNING"
elseif priority==2 then return "INFO"
elseif priority==3 then return "DEBUG"
else error(table.concat({"Priority", priority, "not found"}, " ")) return ""
end
end
---@param message string
---@param priority number
local function debug_print(message, priority)
if (print_priority >= priority) then
print_fun("[" .. Priority.get_label(priority) .. "] " .. message)
end
end
---@param str string
---@return string
local function fix_utf16_string(str)
return str:gsub("\\u(%w%w%w%w)", function (match)
return string.char(tonumber(match,16))
end)
end
--Forward declaration
---@type Request
local Request = {}
---@type table<string, ArkhamImportTaboo>
local taboo_list = {}
---@type number
local deck_type_button_index
local is_private_deck = true
function on_decktype_checkbox_clicked()
self:editButton {
label = is_private_deck and "Published" or "Private",
index = deck_type_button_index
}
is_private_deck = not is_private_deck
end
---@return ArkhamImportConfiguration
local function get_configuration()
local configuration = getObjectsWithTag(tags.configuration)[1]:getTable("configuration")
print_priority = configuration.priority
return configuration
end
---@param configuration ArkhamImportConfiguration
local function initialize(_, configuration)
local builder = getObjectFromGUID(configuration.ui_builder_guid)
deck_type_button_index = builder:call("create_ui", {
target_guid = self:getGUID(),
debug_deck_id = configuration.debug_deck_id,
checkbox_toggle_callback_name = "on_decktype_checkbox_clicked",
build_deck_callback_name = "build_deck"
})
end
function onLoad()
Wait.frames(function ()
local configuration = get_configuration()
local taboo = Request.start({configuration.api_uri, configuration.taboo}, function (status)
local json = JSON.decode(fix_utf16_string(status.text))
for _, taboo in pairs(json) do
---@type <string, boolean>
local cards = {}
for _, card in pairs(JSON.decode(taboo.cards)) do
cards[card.code] = true
end
taboo_list[taboo.id] = {
date = taboo.date_start,
cards = cards
}
end
return true, nil
end)
taboo:with(initialize, nil, configuration)
end, 1)
end
---@param status WebRequestStatus
---@param number number
---@param is_bonded boolean
---@return boolean, ArkhamImportCard
local function on_card_request(status, number, is_bonded)
local text = fix_utf16_string(status.text)
---@type ArkhamImportCard
local card = JSON.decode(text)
card.count = number
card.is_bonded = is_bonded
return true, card
end
---@param configuration ArkhamImportConfiguration
---@param card_code string
---@param count number
---@param is_bonded boolean
---@return Request
local function add_card(configuration, card_code, count, is_bonded)
local api, card_path = configuration.api_uri, configuration.cards
local request = Request.start({api, card_path, card_code}, on_card_request, nil, count, is_bonded)
return request
end
---@param source TTSObject
---@param count number
---@param zones ArkhamImportZone[]
---@param keep_card boolean
---@return fun(card: TTSObject)
local function position_card(source, count, zones, keep_card)
---@param card TTSObject
return function (card)
for n = 1, count do
local zone = zones[n]
local destination = zone.is_absolute and zone.position or self:positionToWorld(zone.position)
local rotation = self:getRotation() + Vector(0, 0, zone.is_facedown and 180 or 0)
card:clone {
position = destination,
rotation = rotation
}
end
if keep_card then source:putObject(card) else card:destruct() end
end
end
---@param name string
---@param index number
---@param source TTSObject
---@param zone ArkhamImportZone
---@param count number
local function take_card(name, index, source, zone, count)
source:takeObject {
position = {0, 1.5, 0},
index = index,
smooth = false,
callback_function = position_card(source, count, zone, true)
}
debug_print(table.concat({ "Added", count, "of", name}, " "), Priority.DEBUG)
end
---@param source TTSObject
---@param target_name string
---@param target_subname string
---@param count number
---@param zone ArkhamImportZone[]
local function process_card(source, target_name, target_subname, count, zone)
---@type GetObjectResults[]
local partial_matches = {}
for _, card in ipairs(source:getObjects()) do
if (card.name == target_name and (not target_subname or card.description==target_subname)) then
return take_card(target_name, card.index, source, zone, count)
elseif card.name == target_name then
table.insert(partial_matches, card)
end
end
local match_count = #partial_matches
if match_count>1 then
debug_print(table.concat {"Found multiple cards with name \"", target_name, "\" none of which matched the given subtitle of \"", target_subname, "\". One or more of the cards in Tabletop Simulator likely has the wrong text in its description."}, Priority.WARNING)
elseif match_count==1 then
take_card(target_name, partial_matches[1], source, zone, count)
else
debug_print(table.concat({ "Card not found:", target_name}, " "), Priority.WARNING)
end
end
---@param source TTSObject
---@param zones ArkhamImportZone[]
local function random_weakness(source, zones)
source:shuffle()
local card = source:takeObject {
position = {0, 1.5, 0},
index = 0,
smooth = false,
callback_function = position_card(source, 1, zones, false),
}
broadcastToAll("Drew random basic weakness: " .. card:getName())
end
---@param configuration ArkhamImportConfiguration
---@param card_id string
---@param used_bindings table<string, boolean>
---@param requests Request[]
local function process_bindings(configuration, card_id, used_bindings, requests)
local bondedCards = configuration.bonded_cards[card_id]
if not bondedCards then return end
if bondedCards.code then bondedCards = {bondedCards} end
for _, bond in ipairs(bondedCards) do
if not used_bindings[bond.code] then
used_bindings[bond.code] = true
local result = add_card(configuration, bond.code, bond.count, true)
table.insert(requests, result)
end
end
end
---@param configuration ArkhamImportConfiguration
---@param slots table<string, number>
---@return Request[]
local function load_cards(configuration, slots)
---@type Request[]
local requests = {}
---@type <string, boolean>
local used_bindings = {} -- Bonded cards that we've already processed
for card_id, number in pairs(slots) do
table.insert(requests, add_card(configuration, card_id, number, false))
process_bindings(configuration, card_id, used_bindings, requests)
end
return requests
end
---@type string[]
local parallel_component = {"", " (Parallel Back)", " (Parallel Front)", " (Parallel)"}
---@param discriminators table<string, string>
---@param card ArkhamImportCard
---@param taboo ArkhamImportTaboo
---@param meta table<string, any>
---@return string, string|nil
local function get_card_selector(discriminators, card, taboo, meta)
local discriminator = discriminators[card.code]
if card.type_code == "investigator" then
local parallel = (meta.alternate_front and 2 or 0) + (meta.alternate_back and 1 or 0)
return table.concat {card.real_name, parallel_component[parallel]}, nil
end
local xp_component = ""
if ((tonumber(card.xp) or 0) > 0) then
xp_component = table.concat {" (", card.xp, ")"}
end
local taboo_component = ""
local cards = taboo.cards or {}
if (cards[card.code]) then
taboo_component = " (Taboo)"
end
local target_name = table.concat({ card.real_name, xp_component, taboo_component })
local target_subname = discriminator or card.subname
return target_name, target_subname
end
---@param zone string
---@param count number
---@return string[]
local function fill_zone(zone, count)
local result = {}
for n=1,count do
result[n] = zone
end
return result
end
---@param card ArkhamImportCard
---@param zone string[]
---@param override string[]
---@return string[]
local function get_zone_id(card, zone, override)
local result = {}
for n=1,card.count do
result[n] = zone[n]
or override[n]
or (card.is_bonded and "bonded")
or (card.permanent and "permanent")
or (card.subtype_name and card.subtype_name:find("Weakness") and "weakness")
or (card.type_code == "investigator" and "investigator")
or "default"
end
return result
end
---@param cards ArkhamImportCard[]
---@param deck ArkhamImportDeck
---@param command_manager TTSObject
---@param configuration ArkhamImportConfiguration
local function on_cards_ready(cards, deck, command_manager, configuration)
local card_bag = getObjectFromGUID(configuration.card_bag_guid)
local weakness_bag = getObjectFromGUID(configuration.weaknesses_bag_guid)
local investigator_bag = getObjectFromGUID(configuration.investigator_bag_guid)
local minicard_bag = getObjectFromGUID(configuration.minicard_bag_guid)
local taboo = taboo_list[deck.taboo_id] or {}
local meta = deck.meta and JSON.decode(deck.meta) or {}
for _, card in ipairs(cards) do
---@type ArkhamImport_Command_HandlerArguments
local parameters = {
configuration = configuration,
source_guid = self:getGUID(),
zone = {},
card = card,
}
---@type ArkhamImport_CommandManager_HandlerResults
local command_result = command_manager:call("handle", parameters)
if not command_result.is_successful then
debug_print(command_result.error_message, Priority.ERROR)
return
end
local card = command_result.card
if not command_result.handled then
local target_name, target_subname = get_card_selector(configuration.discriminators, card, taboo, meta)
local override = configuration.default_zone_overrides[card.code]
if type(override)=="string" then override = fill_zone(override, card.count) end
local zone = get_zone_id(card, command_result.zone, configuration.default_zone_overrides[card.code] or {})
local spawn_zones = {}
local zones = configuration.zones
for index, zone in ipairs(zone) do
spawn_zones[index] = zones[zone]
end
if card.real_name == "Random Basic Weakness" then
random_weakness(weakness_bag, spawn_zones)
elseif card.type_code == "investigator" then
process_card(investigator_bag, target_name, nil, card.count, spawn_zones)
process_card(minicard_bag, card.real_name, nil, card.count, spawn_zones)
else
process_card(card_bag, target_name, target_subname, card.count, spawn_zones)
end
end
end
end
---@param deck ArkhamImportDeck
---@param configuration ArkhamImportConfiguration
local function on_deck_result(deck, configuration)
debug_print(table.concat({ "Found decklist: ", deck.name}), Priority.INFO)
debug_print(table.concat({"-", deck.name, "-"}), Priority.DEBUG)
for k,v in pairs(deck) do
if type(v)=="table" then
debug_print(table.concat {k, ": <table>"}, Priority.DEBUG)
else
debug_print(table.concat {k, ": ", tostring(v)}, Priority.DEBUG)
end
end
debug_print("", Priority.DEBUG)
local investigator_id = deck.investigator_code
local slots = deck.slots
slots[investigator_id] = 1
---@type ArkhamImportCard[]
local requests = load_cards(configuration, deck.slots)
local command_manager = getObjectFromGUID(configuration.command_manager_guid)
---@type ArkhamImport_CommandManager_InitializationArguments
local parameters = {
configuration = configuration,
description = deck.description_md,
}
---@type ArkhamImport_CommandManager_InitializationResults
local results = command_manager:call("initialize", parameters)
if not results.is_successful then
debug_print(results.error_message, Priority.ERROR)
return
end
Request.with_all(requests, on_cards_ready, nil, deck, command_manager, results.configuration)
end
function build_deck()
local configuration = get_configuration()
local deck_id = self:getInputs()[1].value
local deck_uri = { configuration.api_uri, is_private_deck and configuration.private_deck or configuration.public_deck, deck_id }
local deck = Request.start(deck_uri, function (status)
if string.find(status.text, "<!DOCTYPE html>") then
return false, table.concat({ "Private deck ", deck_id, " is not shared"})
end
local json = JSON.decode(status.text)
if not json then
return false, "Deck not found!"
end
return true, JSON.decode(status.text)
end, function (status) end)
deck:with(on_deck_result, nil, configuration)
end
---@type Request
Request = {
is_done = false,
is_successful = false
}
--- Creates a new instance of a Request. Should not be directly called. Instead use Request.start and Request.deferred.
---@param uri string
---@param configure fun(request: Request, status: WebRequestStatus)
---@return Request
function Request:new(uri, configure)
local this = {}
setmetatable(this, self)
self.__index = self
if type(uri)=="table" then
uri = table.concat(uri, "/")
end
this.uri = uri
debug_print(table.concat({"Opened connection to: ", this.uri}), Priority.DEBUG)
WebRequest.get(uri, function(status)
configure(this, status)
end)
return this
end
--- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.
--- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)
---@param uri string
---@param on_success fun(request: Request, status: WebRequestStatus, vararg any)
---@param on_error fun(status: WebRequestStatus)|nil
---@vararg any[]
---@return Request
function Request.deferred(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request:new(uri, function (request, status)
if (status.is_done) then
if (status.is_error) then
request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error
request.is_successful = false
request.is_done = true
else
on_success(request, status)
end
end
end)
end
--- Creates a new request. on_success should return weather the resultant data is as expected, and the processed content of the request.
---@param uri string
---@param on_success fun(status: WebRequestStatus, vararg any): boolean, any
---@param on_error nil|fun(status: WebRequestStatus, vararg any): string
---@vararg any[]
---@return Request
function Request.start(uri, on_success, on_error, ...)
local parameters = table.pack(...)
return Request.deferred(uri, function(request, status)
local result, message = on_success(status, table.unpack(parameters))
if not result then request.error_message = message else request.content = message end
request.is_successful = result
request.is_done = true
end, on_error, table.unpack(parameters))
end
---@param requests Request[]
---@param on_success fun(content: any[], vararg any[])
---@param on_error fun(requests: Request[], vararg any[])|nil
---@vararg any
function Request.with_all(requests, on_success, on_error, ...)
local parameters = table.pack(...)
Wait.condition(function ()
---@type any[]
local results = {}
---@type Request[]
local errors = {}
for _, request in ipairs(requests) do
if request.is_successful then
table.insert(results, request.content)
else
table.insert(errors, request)
end
end
if (#errors<=0) then
on_success(results, table.unpack(parameters))
elseif on_error ==nil then
for _, request in ipairs(errors) do
debug_print(table.concat({ "[ERROR]", request.uri, ":", request.error_message }), Priority.ERROR)
end
else
on_error(requests, table.unpack(parameters))
end
end, function ()
for _, request in ipairs(requests) do
if not request.is_done then return false end
end
return true
end)
end
---@param callback fun(content: any, vararg any)
---@param error_handler fun(error_message: string, vararg any)
function Request:with(callback, error_handler, ...)
local arguments = table.pack(...)
Wait.condition(function ()
if self.is_successful then
callback(self.content, table.unpack(arguments))
else
if error_handler then error_handler(self.error_message, table.unpack(arguments)) else debug_print(self.error_message, Priority.ERROR) end
end
end, function () return self.is_done
end)
end