570 lines
18 KiB
Plaintext
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
|