Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
91c7536789 | ||
![]() |
7d77acaccc | ||
![]() |
4ce8e19697 | ||
![]() |
c746e38cc9 | ||
![]() |
f793d8ef7e | ||
![]() |
15062976ca | ||
![]() |
ca301e1745 | ||
![]() |
40625ab26f | ||
![]() |
3387eb2c26 | ||
![]() |
cc1069734c | ||
![]() |
744cdb2085 | ||
![]() |
802869bf23 | ||
![]() |
7e912d532f | ||
![]() |
87b6cf80e8 | ||
![]() |
083c49f916 | ||
![]() |
e20d054f7b | ||
![]() |
42991138d9 | ||
![]() |
1b4fe1b61b | ||
![]() |
68105ed4cc | ||
![]() |
f971705548 | ||
![]() |
b9d8851338 | ||
![]() |
a1c78edda5 |
38 changed files with 1009 additions and 191 deletions
81
data/lua/core/plugin.lua
Normal file
81
data/lua/core/plugin.lua
Normal file
|
@ -0,0 +1,81 @@
|
|||
--[========[Plugin module]========]
|
||||
|
||||
---@alias plugin_idle_function fun(ctx_name:string, events:WMLTable):boolean|nil
|
||||
|
||||
if wesnoth.kernel_type() == "Application Lua Kernel" then
|
||||
print("Loading plugin module...")
|
||||
|
||||
---Yields control back to the game until the next slice.
|
||||
---@return WMLTable
|
||||
---@return plugin_context
|
||||
---@return plugin_info
|
||||
function wesnoth.plugin.next_slice()
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
---@param cond fun(info:plugin_info):boolean
|
||||
---@param idle plugin_idle_function
|
||||
---@return WMLTable
|
||||
---@return plugin_context
|
||||
---@return plugin_info
|
||||
local function wait_until(cond, idle)
|
||||
local events, context, info = {}, nil, nil
|
||||
repeat
|
||||
local new_events
|
||||
new_events, context, info = wesnoth.plugin.next_slice()
|
||||
for i = 1, #new_events do
|
||||
events[#events + 1] = new_events[i]
|
||||
end
|
||||
if idle then
|
||||
if idle(info.name, events) then
|
||||
break
|
||||
end
|
||||
end
|
||||
if info.name == 'Dialog' then
|
||||
context.skip_dialog{}
|
||||
end
|
||||
until cond(info)
|
||||
return events, context, info
|
||||
end
|
||||
|
||||
---Waits until the plugin reaches a specified context.
|
||||
---Unless the idle function returns true, the context returned from this function is
|
||||
---guaranteed to have the expected value.
|
||||
---@param ctx string The context to wait for.
|
||||
---@param idle plugin_idle_function A function that will be called on each slice, taking as argument the events since the previous slice, and the name of the latest context. It can return true to break out of the wait.
|
||||
---@return WMLTable #All the events that occurred while waiting
|
||||
---@return plugin_context #The most recent context
|
||||
---@return plugin_info #The most recent info
|
||||
function wesnoth.plugin.wait_until(ctx, idle)
|
||||
return wait_until(function(info) return info.name == ctx end, idle)
|
||||
end
|
||||
|
||||
---Waits until the plugin reaches one of several specified contexts.
|
||||
---Unless the idle function returns true, the context returned from this function is
|
||||
---guaranteed to have the expected value.
|
||||
---@param ctx string[] The contexts to wait for.
|
||||
---@param idle plugin_idle_function A function that will be called on each slice, taking as argument the events since the previous slice, and the name of the latest context. It can return true to break out of the wait.
|
||||
---@return WMLTable #All the events that occurred while waiting
|
||||
---@return plugin_context #The most recent context
|
||||
---@return plugin_info #The most recent info
|
||||
function wesnoth.plugin.wait_until_any(ctx, idle)
|
||||
return wait_until(function(info)
|
||||
for i = 1, #ctx do
|
||||
if info.name == ctx[i] then return true end
|
||||
end
|
||||
return false
|
||||
end, idle)
|
||||
end
|
||||
|
||||
---Waits until the plugin reaches a specified context.
|
||||
---Unless the idle function returns true, the context returned from this function is
|
||||
---guaranteed to have the expected value.
|
||||
---@param ctx string The context to wait for.
|
||||
---@param idle plugin_idle_function A function that will be called on each slice, taking as argument the events since the previous slice, and the name of the latest context. It can return true to break out of the wait.
|
||||
---@return WMLTable #All the events that occurred while waiting
|
||||
---@return plugin_context #The most recent context
|
||||
---@return plugin_info #The most recent info
|
||||
function wesnoth.plugin.wait_until_not(ctx, idle)
|
||||
return wait_until(function(info) return info.name ~= ctx end, idle)
|
||||
end
|
||||
end
|
|
@ -9,8 +9,6 @@ local function plugin()
|
|||
|
||||
local counter = 0
|
||||
|
||||
local events, context, info
|
||||
|
||||
local function idle_text(text)
|
||||
counter = counter + 1
|
||||
if counter >= 100 then
|
||||
|
@ -21,59 +19,56 @@ local function plugin()
|
|||
|
||||
log("hello world")
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for titlescreen or lobby")
|
||||
until info.name == "titlescreen" or info.name == "Multiplayer Lobby"
|
||||
local events, context, info = wesnoth.plugin.wait_until_any({"titlescreen", "Multiplayer Lobby"}, function(name)
|
||||
idle_text("in " .. name .. " waiting for titlescreen or lobby")
|
||||
end)
|
||||
|
||||
local tries = 0
|
||||
while info.name == "titlescreen" and tries < 100 do
|
||||
context.play_multiplayer({})
|
||||
context.play_multiplayer{}
|
||||
tries = tries + 1
|
||||
log("playing multiplayer...")
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
end
|
||||
if info.name == "titlescreen" then
|
||||
context.exit({code = 1})
|
||||
coroutine.yield()
|
||||
context.exit{code = 1}
|
||||
return
|
||||
end
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for lobby")
|
||||
until info.name == "Multiplayer Lobby"
|
||||
events, context, info = wesnoth.plugin.wait_until("Multiplayer Lobby", function(name)
|
||||
idle_text("in " .. name .. " waiting for lobby")
|
||||
end)
|
||||
|
||||
context.chat({message = "hosting"})
|
||||
context.chat{message = "hosting"}
|
||||
log("creating a game")
|
||||
context.create({})
|
||||
context.create{}
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for create")
|
||||
until info.name == "Multiplayer Create"
|
||||
events, context, info = wesnoth.plugin.wait_until("Multiplayer Create", function(name)
|
||||
idle_text("in " .. name .. " waiting for create")
|
||||
end)
|
||||
|
||||
context.select_type({type = "scenario"})
|
||||
local s = info.find_level({id = "test1"})
|
||||
context.select_type{type = "scenario"}
|
||||
local s = info.find_level{id = "test1"}
|
||||
if s.index < 0 then
|
||||
log(" error: Could not find scenario with id=test1")
|
||||
end
|
||||
context.select_level({index = s.index})
|
||||
context.select_level{index = s.index}
|
||||
|
||||
log("configuring a game")
|
||||
context.set_name({name = "Test"})
|
||||
context.update_settings({registered_users = false})
|
||||
context.set_name{name = "Test"}
|
||||
context.update_settings{registered_users = false}
|
||||
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
|
||||
context.create({})
|
||||
context.create{}
|
||||
|
||||
local ready = nil
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
for i,v in ipairs(events) do
|
||||
if v[1] == "chat" then
|
||||
std_print(events[i][2])
|
||||
if v[2].message == "ready" then
|
||||
if v.tag == "chat" then
|
||||
std_print(events[i].contents.message)
|
||||
if v.contents.message == "ready" then
|
||||
ready = true
|
||||
end
|
||||
end
|
||||
|
@ -82,31 +77,28 @@ local function plugin()
|
|||
until ready
|
||||
|
||||
log("starting game...")
|
||||
context.chat({message = "starting"})
|
||||
context.launch({})
|
||||
context.chat{message = "starting"}
|
||||
context.launch{}
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for game")
|
||||
until info.name == "Game"
|
||||
events, context, info = wesnoth.plugin.wait_until("Game", function(name)
|
||||
idle_text("in " .. name .. " waiting for game")
|
||||
end)
|
||||
|
||||
log("got to a game context...")
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for not game")
|
||||
until info.name ~= "Game"
|
||||
events, context, info = wesnoth.plugin.wait_until_not("Game", function(name)
|
||||
idle_text("in " .. name .. " waiting for not game")
|
||||
end)
|
||||
|
||||
log("left a game context...")
|
||||
|
||||
repeat
|
||||
context.quit({})
|
||||
log("quitting a " .. info.name .. " context...")
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
until info.name == "titlescreen"
|
||||
|
||||
context.exit({code = 0})
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
return plugin
|
||||
|
|
|
@ -9,8 +9,6 @@ local function plugin()
|
|||
|
||||
local counter = 0
|
||||
|
||||
local events, context, info
|
||||
|
||||
local function find_test_game(game_info)
|
||||
local g = game_info.game_list()
|
||||
if g then
|
||||
|
@ -18,8 +16,8 @@ local function plugin()
|
|||
if gamelist then
|
||||
for i = 1, #gamelist do
|
||||
local t = gamelist[i]
|
||||
if t[1] == "game" then
|
||||
local game = t[2]
|
||||
if t.tag == "game" then
|
||||
local game = t.contents
|
||||
if game.scenario == "Test" then
|
||||
return game.id
|
||||
end
|
||||
|
@ -40,41 +38,39 @@ local function plugin()
|
|||
|
||||
log("hello world")
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for titlescreen or lobby")
|
||||
until info.name == "titlescreen" or info.name == "Multiplayer Lobby"
|
||||
local events, context, info = wesnoth.plugin.wait_until_any({"titlescreen", "Multiplayer Lobby"}, function(name)
|
||||
idle_text("in " .. name .. " waiting for titlescreen or lobby")
|
||||
end)
|
||||
|
||||
local tries = 0
|
||||
while info.name == "titlescreen" and tries < 100 do
|
||||
context.play_multiplayer({})
|
||||
context.play_multiplayer{}
|
||||
tries = tries + 1
|
||||
log("playing multiplayer...")
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
end
|
||||
if info.name == "titlescreen" then
|
||||
context.exit({code = 1})
|
||||
coroutine.yield()
|
||||
context.exit{code = 1}
|
||||
return
|
||||
end
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for lobby")
|
||||
until info.name == "Multiplayer Lobby"
|
||||
events, context, info = wesnoth.plugin.wait_until("Multiplayer Lobby", function(name)
|
||||
idle_text("in " .. name .. " waiting for lobby")
|
||||
end)
|
||||
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
|
||||
context.chat({message = "waiting for test game to join..."})
|
||||
context.chat{message = "waiting for test game to join..."}
|
||||
|
||||
local test_game = nil
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
idle_text("in " .. info.name .. " waiting for test game")
|
||||
|
||||
for i,v in ipairs(events) do
|
||||
if v[1] == "chat" then
|
||||
std_print("chat:", v[2].message)
|
||||
if v.tag == "chat" then
|
||||
std_print("chat:", v.contents.message)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -82,76 +78,70 @@ local function plugin()
|
|||
until test_game
|
||||
|
||||
log("found a test game, joining... id = " .. test_game)
|
||||
context.chat({message = "found test game"})
|
||||
context.select_game({id = test_game})
|
||||
context.chat{message = "found test game"}
|
||||
context.select_game{id = test_game}
|
||||
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
|
||||
context.chat({message = "going to join"})
|
||||
context.chat{message = "going to join"}
|
||||
|
||||
context.join({})
|
||||
context.join{}
|
||||
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
|
||||
-- Don't know why THIS context has to chat member but it doesn't
|
||||
-- Don't know why THIS context has no chat member but it doesn't
|
||||
-- Adding the guard if to bypass a script crash and get mp_tests running.
|
||||
-- GAL 28NOV2017
|
||||
if context.chat then
|
||||
context.chat({message = "done first join"})
|
||||
context.chat{message = "done first join"}
|
||||
end
|
||||
|
||||
while not (info.name == "Dialog" or info.name == "Multiplayer Join") do
|
||||
if context.join then
|
||||
context.join({})
|
||||
context.join{}
|
||||
else
|
||||
std_print("did not find join...")
|
||||
end
|
||||
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
idle_text("in " .. info.name .. " waiting for leader select dialog")
|
||||
end
|
||||
|
||||
if info.name == "Dialog" then
|
||||
log("got a leader select dialog...")
|
||||
context.skip_dialog({})
|
||||
events, context, info = coroutine.yield()
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for mp join")
|
||||
until info.name == "Multiplayer Join"
|
||||
log("got a leader select dialog... id=" .. info.id().id)
|
||||
context.skip_dialog{}
|
||||
events, context, info = wesnoth.plugin.wait_until("Multiplayer Join", function(name)
|
||||
idle_text("in " .. name .. " waiting for mp join")
|
||||
end)
|
||||
end
|
||||
|
||||
log("got to multiplayer join...")
|
||||
context.chat({message = "ready"})
|
||||
context.chat{message = "ready"}
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for game")
|
||||
until info.name == "Game"
|
||||
events, context, info = wesnoth.plugin.wait_until("Game", function(name)
|
||||
idle_text("in " .. name .. " waiting for game")
|
||||
end)
|
||||
|
||||
log("got to a game context...")
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
idle_text("in " .. info.name .. " waiting for the last scenario")
|
||||
until info.scenario_name ~= nil and info.scenario_name().scenario_name == "Multiplayer Unit Test test2"
|
||||
|
||||
repeat
|
||||
events, context, info = coroutine.yield()
|
||||
idle_text("in " .. info.name .. " waiting for not game")
|
||||
until info.name ~= "Game"
|
||||
events, context, info = wesnoth.plugin.wait_until_not("Game", function(name)
|
||||
idle_text("in " .. name .. " waiting for not game")
|
||||
end)
|
||||
|
||||
log("left a game context...")
|
||||
|
||||
repeat
|
||||
context.quit({})
|
||||
context.quit{}
|
||||
log("quitting a " .. info.name .. " context...")
|
||||
events, context, info = coroutine.yield()
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
until info.name == "titlescreen"
|
||||
|
||||
context.exit({code = 0})
|
||||
coroutine.yield()
|
||||
context.exit{code = 0}
|
||||
end
|
||||
|
||||
return plugin
|
||||
|
|
156
data/test/plugin/start-campaign.lua
Normal file
156
data/test/plugin/start-campaign.lua
Normal file
|
@ -0,0 +1,156 @@
|
|||
-- start-campaign.lua --
|
||||
-- Try to start a campaign, recruit a unit, move to a village, and end turn
|
||||
|
||||
local function plugin(events, context, info)
|
||||
local function log(text)
|
||||
std_print("start-campaign: " .. text)
|
||||
end
|
||||
|
||||
local counter = 0
|
||||
|
||||
local function idle_text(text)
|
||||
counter = counter + 1
|
||||
if counter >= 100 then
|
||||
counter = 0
|
||||
log("idling " .. text)
|
||||
end
|
||||
end
|
||||
|
||||
log("hello world from " .. info.name)
|
||||
|
||||
events, context, info = wesnoth.plugin.wait_until("titlescreen", function(name)
|
||||
idle_text("in " .. name .. " waiting for titlescreen")
|
||||
end)
|
||||
|
||||
local args = info.command_line().args or {}
|
||||
local campaign_id = args[1] or "The_South_Guard"
|
||||
|
||||
local tries = 0
|
||||
while info.name == "titlescreen" and tries < 100 do
|
||||
context.play_campaign({})
|
||||
tries = tries + 1
|
||||
log("playing campaign...")
|
||||
events, context, info = coroutine.yield()
|
||||
end
|
||||
if info.name == "titlescreen" then
|
||||
context.exit{code = 1}
|
||||
return
|
||||
end
|
||||
|
||||
events, context, info = wesnoth.plugin.wait_until("Campaign Selection", function(name)
|
||||
idle_text("in " .. name .. " waiting for campaign_selection")
|
||||
end)
|
||||
|
||||
local s = info.find_level{id = campaign_id}
|
||||
if s.index < 0 then
|
||||
log(" error: Could not find campaign with id=" .. campaign_id)
|
||||
end
|
||||
log("selected "..campaign_id)
|
||||
context.select_level({index = s.index})
|
||||
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
|
||||
log("creating game")
|
||||
context.create{}
|
||||
std_print('A')
|
||||
|
||||
events, context, info = wesnoth.plugin.wait_until_any({"Game", "Campaign Configure"}, function(name)
|
||||
idle_text("in " .. name .. " waiting for game or configure")
|
||||
end)
|
||||
|
||||
std_print('B')
|
||||
if info.name == "Campaign Configure" then
|
||||
log("skipping configure")
|
||||
context.launch{}
|
||||
events, context, info = wesnoth.plugin.wait_until("Game", function(name)
|
||||
idle_text("in " .. name .. " waiting for game")
|
||||
end)
|
||||
end
|
||||
std_print('C')
|
||||
|
||||
log("got to a game context...")
|
||||
|
||||
repeat
|
||||
idle_text("in " .. info.name .. ", waiting to gain control")
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
if info.name == "Dialog" then
|
||||
context.skip_dialog{}
|
||||
end
|
||||
until info.name == "Game" and info.can_move().can_move
|
||||
wesnoth.game_config.debug = true
|
||||
|
||||
local my_side, start_loc, keep_loc, on_keep, castle_loc, first_recruit
|
||||
wesnoth.plugin.execute(context, function()
|
||||
log('finding a spot to recruit')
|
||||
my_side = wesnoth.current.side
|
||||
std_print('my_side='..my_side)
|
||||
start_loc = wesnoth.current.map.special_locations[my_side]
|
||||
std_print('start_loc=('..start_loc.x..','..start_loc.y..')')
|
||||
local ai = wesnoth.sides.debug_ai(my_side).ai
|
||||
std_print('ai='..tostring(ai))
|
||||
local x,y = ai.suitable_keep(wesnoth.units.get(start_loc))
|
||||
if x and y then
|
||||
keep_loc = wesnoth.named_tuple({x,y}, {'x','y'})
|
||||
std_print('keep_loc=('..keep_loc.x..','..keep_loc.y..')')
|
||||
end
|
||||
on_keep = start_loc == keep_loc
|
||||
std_print('on_keep='..tostring(on_keep))
|
||||
castle_loc = wesnoth.map.find{formula = 'castle', wml.tag.filter_adjacent{x = keep_loc.x, y = keep_loc.y}}[1]
|
||||
std_print('castle_loc=('..castle_loc.x..','..castle_loc.y..')')
|
||||
first_recruit = wesnoth.sides[my_side].recruit[1]
|
||||
std_print('first_recruit='..first_recruit)
|
||||
end)
|
||||
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
log(wesnoth.as_text(events))
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
|
||||
if keep_loc then
|
||||
log("found a suitable keep at (" .. keep_loc.x .. "," .. keep_loc.y .. ")")
|
||||
else
|
||||
log("didn't find a suitable keep")
|
||||
end
|
||||
|
||||
while not on_keep do
|
||||
context.synced_command{wml.tag.move{x = {start_loc.x, keep_loc.x}, y = {start_loc.y, keep_loc.y}}}
|
||||
wesnoth.plugin.execute(context, function()
|
||||
local u = wesnoth.units.get(keep_loc)
|
||||
on_keep = u and u.side == my_side and u.canrecruit
|
||||
end)
|
||||
events, context, info = wesnoth.plugins.next_slice()
|
||||
end
|
||||
|
||||
log("recruiting a " .. first_recruit)
|
||||
context.synced_command{
|
||||
wml.tag.recruit{type = first_recruit, x = castle_loc.x, y = castle_loc.y, wml.tag.from{x = keep_loc.x, y = keep_loc.y}},
|
||||
}
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
log("ending turn")
|
||||
context.end_turn{}
|
||||
|
||||
repeat
|
||||
idle_text("in " .. info.name .. ", waiting to gain control")
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
if info.name == "Dialog" then
|
||||
context.skip_dialog{}
|
||||
end
|
||||
until info.name == "Game" and info.can_move().can_move
|
||||
|
||||
context.quit{}
|
||||
|
||||
events, context, info = wesnoth.plugin.wait_until_not("Game", function(name)
|
||||
idle_text("in " .. name .. " waiting for not game")
|
||||
end)
|
||||
|
||||
log("left a game context...")
|
||||
|
||||
while info.name ~= "titlescreen" do
|
||||
log("quitting a " .. info.name .. " context...")
|
||||
context.quit{}
|
||||
events, context, info = wesnoth.plugin.next_slice()
|
||||
end
|
||||
|
||||
context.exit{code = 0}
|
||||
end
|
||||
|
||||
return plugin
|
|
@ -232,11 +232,9 @@ uses
|
|||
when connecting to a server, ignoring other preferences. Unsafe.
|
||||
.TP
|
||||
.BI --plugin \ script
|
||||
(experimental) load a
|
||||
load a
|
||||
.I script
|
||||
which defines a Wesnoth plugin. Similar to
|
||||
.BR --script ,
|
||||
but Lua file should return a function which will be run as a coroutine and periodically woken up with updates.
|
||||
which defines a Wesnoth plugin. Lua file should return a function which will be run as a coroutine and periodically woken up with updates.
|
||||
.TP
|
||||
.BI -P,\ --patch \ base-file \ patch-file
|
||||
applies a DiffWML patch to a WML file; does not preprocess either of the files.
|
||||
|
@ -303,11 +301,6 @@ to
|
|||
.I output
|
||||
without initializing a screen.
|
||||
.TP
|
||||
.BI --script \ file
|
||||
(experimental)
|
||||
.I file
|
||||
containing a Lua script to control the client.
|
||||
.TP
|
||||
.BI -s[ host ],\ --server[ =host ]
|
||||
connects to the specified host if any, otherwise connect to the first server in preferences. Example:
|
||||
.B --server
|
||||
|
|
|
@ -1188,6 +1188,7 @@
|
|||
<Unit filename="../../src/tests/test_image_modifications.cpp" />
|
||||
<Unit filename="../../src/tests/test_irdya_date.cpp" />
|
||||
<Unit filename="../../src/tests/test_lexical_cast.cpp" />
|
||||
<Unit filename="../../src/tests/test_lua_ptr.cpp" />
|
||||
<Unit filename="../../src/tests/test_map_location.cpp" />
|
||||
<Unit filename="../../src/tests/test_mp_connect.cpp" />
|
||||
<Unit filename="../../src/tests/test_recall_list.cpp" />
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
0554467DB5FE99D85ABCDCA0 /* edit_pbl_translation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 00574699A982AA23F12B39E0 /* edit_pbl_translation.cpp */; };
|
||||
1234567890ABCDEF12345678 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
|
||||
1234567890ABCDEF12345679 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
|
||||
144E49509EAC409649899BD4 /* test_lua_ptr.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2AC74C76BE76F62C771A81E1 /* test_lua_ptr.cpp */; };
|
||||
172E48A5BD149999CE64EDF8 /* prompt.hpp in Headers */ = {isa = PBXBuildFile; fileRef = D4594633BF3F8A06D6AE752F /* prompt.hpp */; };
|
||||
179D4E93A08C5A67B071C6C1 /* spinner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E4214F3DA80B54080C4B548F /* spinner.cpp */; };
|
||||
19B14238AD52EC06ED2094F1 /* tab_container.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 162C4B1E9F7373592D0F3B89 /* tab_container.cpp */; };
|
||||
|
@ -1605,6 +1606,7 @@
|
|||
20E644DC98F26C756364EC2C /* choose_addon.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = choose_addon.cpp; sourceTree = "<group>"; };
|
||||
26A04033A9545CFE8A226FBD /* test_schema_self_validator.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = test_schema_self_validator.cpp; sourceTree = "<group>"; };
|
||||
27764FB68F02032F1C0B6748 /* statistics_record.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = statistics_record.cpp; sourceTree = "<group>"; };
|
||||
2AC74C76BE76F62C771A81E1 /* test_lua_ptr.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = test_lua_ptr.cpp; sourceTree = "<group>"; };
|
||||
2CFD4922B64EA6C9F71F71A2 /* preferences.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = preferences.hpp; path = preferences/preferences.hpp; sourceTree = "<group>"; };
|
||||
3D284B9A81882806D8B25006 /* spritesheet_generator.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = spritesheet_generator.hpp; sourceTree = "<group>"; };
|
||||
3975405BB582CA290366CD21 /* test_help_markup.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = test_help_markup.cpp; sourceTree = "<group>"; };
|
||||
|
@ -4894,6 +4896,7 @@
|
|||
91E3560E1CACA6CB00774252 /* test_image_modifications.cpp */,
|
||||
4649B879202886F000827CFB /* test_irdya_date.cpp */,
|
||||
B597C4AD0FACD42E00CE81F5 /* test_lexical_cast.cpp */,
|
||||
2AC74C76BE76F62C771A81E1 /* test_lua_ptr.cpp */,
|
||||
91E356111CACA6CB00774252 /* test_map_location.cpp */,
|
||||
91E356121CACA6CB00774252 /* test_mp_connect.cpp */,
|
||||
91E356131CACA6CB00774252 /* test_recall_list.cpp */,
|
||||
|
@ -6681,9 +6684,9 @@
|
|||
DC764C9F94D8B634B47A92B0 /* rich_label.cpp in Sources */,
|
||||
DDA14069BCE29DE0FE71B970 /* gui_test_dialog.cpp in Sources */,
|
||||
DDE34117BDAA30C965F6E4DB /* preferences.cpp in Sources */,
|
||||
4A1D4916A16C7C6E07D0BAB2 /* spritesheet_generator.cpp in Sources */,
|
||||
C3854DF5A850564161932EE5 /* test_help_markup.cpp in Sources */,
|
||||
4A1D4916A16C7C6E07D0BAB2 /* spritesheet_generator.cpp in Sources */,
|
||||
144E49509EAC409649899BD4 /* test_lua_ptr.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -55,6 +55,10 @@
|
|||
argument = "--no-log-to-file"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--plugin="$(PROJECT_DIR)/../../data/test/plugin/start-campaign.lua""
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--data-dir="$(PROJECT_DIR)/../..""
|
||||
isEnabled = "NO">
|
||||
|
|
|
@ -16,6 +16,7 @@ tests/test_help_markup.cpp
|
|||
tests/test_image_modifications.cpp
|
||||
tests/test_irdya_date.cpp
|
||||
tests/test_lexical_cast.cpp
|
||||
tests/test_lua_ptr.cpp
|
||||
tests/test_map_location.cpp
|
||||
tests/test_mp_connect.cpp
|
||||
tests/test_recall_list.cpp
|
||||
|
|
|
@ -158,8 +158,8 @@ static int cfun_ai_get_suitable_keep(lua_State *L)
|
|||
return 0;
|
||||
}
|
||||
else {
|
||||
lua_pushnumber(L, res.wml_x());
|
||||
lua_pushnumber(L, res.wml_y());
|
||||
lua_pushinteger(L, res.wml_x());
|
||||
lua_pushinteger(L, res.wml_y());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,6 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
|
|||
, screenshot(false)
|
||||
, screenshot_map_file()
|
||||
, screenshot_output_file()
|
||||
, script_file()
|
||||
, plugin_file()
|
||||
, script_unsafe_mode(false)
|
||||
, strict_validation(false)
|
||||
|
@ -196,13 +195,12 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
|
|||
("nomusic", "runs the game without music.")
|
||||
("nosound", "runs the game without sounds and music.")
|
||||
("password", po::value<std::string>(), "uses <password> when connecting to a server, ignoring other preferences.")
|
||||
("plugin", po::value<std::string>(), "(experimental) load a script which defines a wesnoth plugin. similar to --script below, but Lua file should return a function which will be run as a coroutine and periodically woken up with updates.")
|
||||
("plugin", po::value<std::string>(), "load a script which defines a wesnoth plugin. Lua file should return a function which will be run as a coroutine and periodically woken up with updates.")
|
||||
("render-image", po::value<two_strings>()->multitoken(), "takes two arguments: <image> <output>. Like screenshot, but instead of a map, takes a valid Wesnoth 'image path string' with image path functions, and writes it to a .png file." IMPLY_TERMINAL)
|
||||
("generate-spritesheet", po::value<std::string>(), "generates a spritesheet from all png images in the given path, recursively (one sheet per directory)")
|
||||
("report,R", "initializes game directories, prints build information suitable for use in bug reports, and exits." IMPLY_TERMINAL)
|
||||
("rng-seed", po::value<unsigned int>(), "seeds the random number generator with number <arg>. Example: --rng-seed 0")
|
||||
("screenshot", po::value<two_strings>()->multitoken(), "takes two arguments: <map> <output>. Saves a screenshot of <map> to <output> without initializing a screen. Editor must be compiled in for this to work." IMPLY_TERMINAL)
|
||||
("script", po::value<std::string>(), "(experimental) file containing a Lua script to control the client")
|
||||
("server,s", po::value<std::string>()->implicit_value(std::string()), "connects to the host <arg> if specified or to the first host in your preferences.")
|
||||
("strict-validation", "makes validation errors fatal")
|
||||
("translations-over", po::value<unsigned int>(), "Specify the standard for determining whether a translation is complete.")
|
||||
|
@ -458,8 +456,6 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
|
|||
screenshot_map_file = vm["screenshot"].as<two_strings>().first;
|
||||
screenshot_output_file = vm["screenshot"].as<two_strings>().second;
|
||||
}
|
||||
if(vm.count("script"))
|
||||
script_file = vm["script"].as<std::string>();
|
||||
if(vm.count("unsafe-scripts"))
|
||||
script_unsafe_mode = true;
|
||||
if(vm.count("plugin"))
|
||||
|
|
|
@ -188,9 +188,7 @@ public:
|
|||
utils::optional<std::string> screenshot_map_file;
|
||||
/** Output file to put screenshot in. Second parameter given after --screenshot. */
|
||||
utils::optional<std::string> screenshot_output_file;
|
||||
/** File to load lua script from. */
|
||||
utils::optional<std::string> script_file;
|
||||
/** File to load a lua plugin (similar to a script) from. Experimental / may replace script. */
|
||||
/** File to load a lua plugin script from. */
|
||||
utils::optional<std::string> plugin_file;
|
||||
/** Whether to load the "package" package for the scripting environment. (This allows to load arbitrary lua packages, and gives untrusted lua the same permissions as wesnoth executable) */
|
||||
bool script_unsafe_mode;
|
||||
|
|
|
@ -338,27 +338,6 @@ bool game_launcher::init_lua_script()
|
|||
plugins_manager::get()->get_kernel_base()->load_package();
|
||||
}
|
||||
|
||||
// get the application lua kernel, load and execute script file, if script file is present
|
||||
if(cmdline_opts_.script_file) {
|
||||
filesystem::scoped_istream sf = filesystem::istream_file(*cmdline_opts_.script_file);
|
||||
|
||||
if(!sf->fail()) {
|
||||
/* Cancel all "jumps" to editor / campaign / multiplayer */
|
||||
jump_to_multiplayer_ = false;
|
||||
jump_to_editor_ = false;
|
||||
jump_to_campaign_.jump = false;
|
||||
|
||||
std::string full_script((std::istreambuf_iterator<char>(*sf)), std::istreambuf_iterator<char>());
|
||||
|
||||
PLAIN_LOG << "\nRunning lua script: " << *cmdline_opts_.script_file;
|
||||
|
||||
plugins_manager::get()->get_kernel_base()->run(full_script.c_str(), *cmdline_opts_.script_file);
|
||||
} else {
|
||||
PLAIN_LOG << "Encountered failure when opening script '" << *cmdline_opts_.script_file << '\'';
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(cmdline_opts_.plugin_file) {
|
||||
std::string filename = *cmdline_opts_.plugin_file;
|
||||
|
||||
|
@ -751,14 +730,20 @@ std::string game_launcher::jump_to_campaign_id() const
|
|||
return jump_to_campaign_.campaign_id;
|
||||
}
|
||||
|
||||
bool game_launcher::play_campaign() {
|
||||
if(new_campaign()) {
|
||||
state_.set_skip_story(jump_to_campaign_.skip_story);
|
||||
jump_to_campaign_.jump = false;
|
||||
launch_game(reload_mode::NO_RELOAD_DATA);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool game_launcher::goto_campaign()
|
||||
{
|
||||
if(jump_to_campaign_.jump) {
|
||||
if(new_campaign()) {
|
||||
state_.set_skip_story(jump_to_campaign_.skip_story);
|
||||
jump_to_campaign_.jump = false;
|
||||
launch_game(reload_mode::NO_RELOAD_DATA);
|
||||
} else {
|
||||
if(!play_campaign()) {
|
||||
jump_to_campaign_.jump = false;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ public:
|
|||
void select_mp_server(const std::string& server) { multiplayer_server_ = server; }
|
||||
bool play_multiplayer(mp_mode mode);
|
||||
bool play_multiplayer_commandline();
|
||||
bool play_campaign();
|
||||
bool change_language();
|
||||
|
||||
void launch_game(reload_mode reload = reload_mode::RELOAD_DATA);
|
||||
|
|
|
@ -39,6 +39,23 @@ namespace gui2::dialogs
|
|||
|
||||
REGISTER_DIALOG(campaign_selection)
|
||||
|
||||
campaign_selection::campaign_selection(ng::create_engine& eng)
|
||||
: modal_dialog(window_id())
|
||||
, engine_(eng)
|
||||
, choice_(-1)
|
||||
, rng_mode_(RNG_DEFAULT)
|
||||
, mod_states_()
|
||||
, page_ids_()
|
||||
, difficulties_()
|
||||
, current_difficulty_()
|
||||
, current_sorting_(RANK)
|
||||
, currently_sorted_asc_(true)
|
||||
, mod_ids_()
|
||||
{
|
||||
set_show_even_without_video(true);
|
||||
set_allow_plugin_skip(false);
|
||||
}
|
||||
|
||||
void campaign_selection::campaign_selected()
|
||||
{
|
||||
tree_view& tree = find_widget<tree_view>("campaign_tree");
|
||||
|
@ -425,6 +442,28 @@ void campaign_selection::pre_show()
|
|||
connect_signal_notify_modified(diff_menu, std::bind(&campaign_selection::difficulty_selected, this));
|
||||
|
||||
campaign_selected();
|
||||
|
||||
plugins_context_.reset(new plugins_context("Campaign Selection"));
|
||||
plugins_context_->set_callback("create", [this](const config&) { set_retval(retval::OK); }, false);
|
||||
plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
|
||||
|
||||
plugins_context_->set_accessor("find_level", [this](const config& cfg) {
|
||||
const std::string id = cfg["id"].str();
|
||||
auto result = engine_.find_level_by_id(id);
|
||||
return config {
|
||||
"index", result.second,
|
||||
"type", level_type::get_string(result.first),
|
||||
};
|
||||
});
|
||||
|
||||
plugins_context_->set_accessor_int("find_mod", [this](const config& cfg) {
|
||||
return engine_.find_extra_by_id(ng::create_engine::MOD, cfg["id"]);
|
||||
});
|
||||
|
||||
plugins_context_->set_callback("select_level", [this](const config& cfg) {
|
||||
choice_ = cfg["index"].to_int();
|
||||
engine_.set_current_level(choice_);
|
||||
}, true);
|
||||
}
|
||||
|
||||
void campaign_selection::add_campaign_to_tree(const config& campaign)
|
||||
|
|
|
@ -18,13 +18,14 @@
|
|||
#include "gui/dialogs/modal_dialog.hpp"
|
||||
|
||||
#include "game_initialization/create_engine.hpp"
|
||||
#include "gui/dialogs/multiplayer/plugin_executor.hpp"
|
||||
|
||||
#include <boost/dynamic_bitset.hpp>
|
||||
|
||||
namespace gui2::dialogs
|
||||
{
|
||||
|
||||
class campaign_selection : public modal_dialog
|
||||
class campaign_selection : public modal_dialog, private plugin_executor
|
||||
{
|
||||
enum CAMPAIGN_ORDER {RANK, DATE, NAME};
|
||||
public:
|
||||
|
@ -41,20 +42,7 @@ public:
|
|||
RNG_BIASED,
|
||||
};
|
||||
|
||||
explicit campaign_selection(ng::create_engine& eng)
|
||||
: modal_dialog(window_id())
|
||||
, engine_(eng)
|
||||
, choice_(-1)
|
||||
, rng_mode_(RNG_DEFAULT)
|
||||
, mod_states_()
|
||||
, page_ids_()
|
||||
, difficulties_()
|
||||
, current_difficulty_()
|
||||
, current_sorting_(RANK)
|
||||
, currently_sorted_asc_(true)
|
||||
, mod_ids_()
|
||||
{
|
||||
}
|
||||
explicit campaign_selection(ng::create_engine& eng);
|
||||
|
||||
/***** ***** ***** setters / getters for members ***** ****** *****/
|
||||
|
||||
|
|
|
@ -93,6 +93,7 @@ loading_screen::loading_screen(std::function<void()> f)
|
|||
|
||||
current_visible_stage_ = visible_stages_.end();
|
||||
singleton_ = this;
|
||||
set_allow_plugin_skip(false);
|
||||
}
|
||||
|
||||
void loading_screen::pre_show()
|
||||
|
|
|
@ -66,20 +66,25 @@ bool modal_dialog::show(const unsigned auto_close_time)
|
|||
{
|
||||
if(video::headless() && !show_even_without_video_) {
|
||||
DBG_DP << "modal_dialog::show denied";
|
||||
if(!allow_plugin_skip_) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if(allow_plugin_skip_) {
|
||||
bool skipped = false;
|
||||
|
||||
plugins_manager* pm = plugins_manager::get();
|
||||
if (pm && pm->any_running())
|
||||
{
|
||||
plugins_context pc("Dialog");
|
||||
pc.set_callback("skip_dialog", [this](config) { retval_ = retval::OK; }, false);
|
||||
pc.set_callback("quit", [](config) {}, false);
|
||||
pc.set_callback("skip_dialog", [this, &skipped](config) { retval_ = retval::OK; skipped = true; }, false);
|
||||
pc.set_callback("quit", [this, &skipped](config) { retval_ = retval::CANCEL; skipped = true; }, false);
|
||||
pc.set_callback("select", [this, &skipped](config c) { retval_ = c["retval"].to_int(); skipped = true; }, false);
|
||||
pc.set_accessor_string("id", [this](config) { return window_id(); });
|
||||
pc.play_slice();
|
||||
}
|
||||
|
||||
return false;
|
||||
if(skipped) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
init_fields();
|
||||
|
|
|
@ -416,9 +416,10 @@ void mp_create_game::pre_show()
|
|||
|
||||
plugins_context_->set_accessor("find_level", [this](const config& cfg) {
|
||||
const std::string id = cfg["id"].str();
|
||||
auto result = create_engine_.find_level_by_id(id);
|
||||
return config {
|
||||
"index", create_engine_.find_level_by_id(id).second,
|
||||
"type", level_type::get_string(create_engine_.find_level_by_id(id).first),
|
||||
"index", result.second,
|
||||
"type", level_type::get_string(result.first),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ mp_join_game::mp_join_game(saved_game& state, wesnothd_connection& connection, c
|
|||
, flg_dialog_(nullptr)
|
||||
{
|
||||
set_show_even_without_video(true);
|
||||
set_allow_plugin_skip(false);
|
||||
}
|
||||
|
||||
mp_join_game::~mp_join_game()
|
||||
|
|
|
@ -28,12 +28,17 @@ sp_options_configure::sp_options_configure(ng::create_engine& create_engine, ng:
|
|||
, config_engine_(config_engine)
|
||||
, options_manager_()
|
||||
{
|
||||
set_show_even_without_video(true);
|
||||
set_allow_plugin_skip(false);
|
||||
}
|
||||
|
||||
void sp_options_configure::pre_show()
|
||||
{
|
||||
options_manager_.reset(new mp_options_helper(*this, create_engine_));
|
||||
options_manager_->update_all_options();
|
||||
|
||||
plugins_context_.reset(new plugins_context("Campaign Configure"));
|
||||
plugins_context_->set_callback("launch", [this](const config&) { set_retval(retval::OK); }, false);
|
||||
}
|
||||
|
||||
void sp_options_configure::post_show()
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
#include "savegame.hpp"
|
||||
#include "scripting/game_lua_kernel.hpp"
|
||||
#include "scripting/plugins/context.hpp"
|
||||
#include "scripting/plugins/manager.hpp"
|
||||
#include "sound.hpp"
|
||||
#include "soundsource.hpp"
|
||||
#include "statistics.hpp"
|
||||
|
@ -280,7 +281,59 @@ void play_controller::init(const config& level)
|
|||
plugins_context_->set_callback("save_game", [this](const config& cfg) { save_game_auto(cfg["filename"]); }, true);
|
||||
plugins_context_->set_callback("save_replay", [this](const config& cfg) { save_replay_auto(cfg["filename"]); }, true);
|
||||
plugins_context_->set_callback("quit", [](const config&) { throw_quit_game_exception(); }, false);
|
||||
plugins_context_->set_callback_execute(*resources::lua_kernel);
|
||||
plugins_context_->set_accessor_string("scenario_name", [this](config) { return get_scenario_name(); });
|
||||
plugins_context_->set_accessor_int("current_side", [this](config) { return current_side(); });
|
||||
plugins_context_->set_accessor_int("current_turn", [this](config) { return turn(); });
|
||||
plugins_context_->set_accessor_bool("can_move", [this](config) { return !events::commands_disabled && gamestate().gamedata_.phase() == game_data::TURN_PLAYING; });
|
||||
plugins_context_->set_callback("end_turn", [this](config) { require_end_turn(); }, false);
|
||||
plugins_context_->set_callback("synced_command", [this](config cmd) {
|
||||
auto& pm = *plugins_manager::get();
|
||||
if(resources::whiteboard->has_planned_unit_map())
|
||||
{
|
||||
ERR_NG << "plugin called synced command while whiteboard is applied, ignoring";
|
||||
pm.notify_event("synced_command_error", config{"error", "whiteboard"});
|
||||
return;
|
||||
}
|
||||
|
||||
auto& gamedata = gamestate().gamedata_;
|
||||
const bool is_too_early = gamedata.phase() == game_data::INITIAL || resources::gamedata->phase() == game_data::PRELOAD;
|
||||
const bool is_during_turn = gamedata.phase() == game_data::TURN_PLAYING;
|
||||
const bool is_unsynced = synced_context::get_synced_state() == synced_context::UNSYNCED;
|
||||
if(is_too_early) {
|
||||
ERR_NG << "synced command called too early, only allowed at START or later";
|
||||
pm.notify_event("synced_command_error", config{"error", "too-early"});
|
||||
return;
|
||||
}
|
||||
if(is_unsynced && !is_during_turn) {
|
||||
ERR_NG << "synced command can only be used during a turn when a user would also be able to invoke commands";
|
||||
pm.notify_event("synced_command_error", config{"error", "not-your-turn"});
|
||||
return;
|
||||
}
|
||||
if(is_unsynced && events::commands_disabled) {
|
||||
ERR_NG << "synced command cannot be invoked while commands are blocked";
|
||||
pm.notify_event("synced_command_error", config{"error", "disabled"});
|
||||
return;
|
||||
}
|
||||
if(is_unsynced && !resources::controller->current_team().is_local()) {
|
||||
ERR_NG << "synced command can only be used from clients that control the currently playing side";
|
||||
pm.notify_event("synced_command_error", config{"error", "not-your-turn"});
|
||||
return;
|
||||
}
|
||||
for(const auto [key, child] : cmd.all_children_range()) {
|
||||
synced_context::run_in_synced_context_if_not_already(
|
||||
/*commandname*/ key,
|
||||
/*data*/ child,
|
||||
/*use_undo*/ true,
|
||||
/*show*/ true,
|
||||
/*error_handler*/ [&pm](const std::string& message) {
|
||||
ERR_NG << "synced command from plugin raised an error: " << message;
|
||||
pm.notify_event("synced_command_error", config{"error", "error", "message", message});
|
||||
}
|
||||
);
|
||||
ai::manager::get_singleton().raise_gamestate_changed();
|
||||
}
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ public:
|
|||
void init_side_end();
|
||||
|
||||
virtual void force_end_turn() = 0;
|
||||
virtual void require_end_turn() = 0;
|
||||
virtual void check_objectives() = 0;
|
||||
|
||||
virtual void on_not_observer() = 0;
|
||||
|
|
|
@ -74,7 +74,7 @@ public:
|
|||
|
||||
void end_turn();
|
||||
void force_end_turn() override;
|
||||
void require_end_turn();
|
||||
void require_end_turn() override;
|
||||
|
||||
class hotkey_handler;
|
||||
std::string describe_result() const;
|
||||
|
|
|
@ -27,8 +27,10 @@
|
|||
#include "scripting/application_lua_kernel.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "game_config.hpp"
|
||||
#include "game_errors.hpp"
|
||||
#include "log.hpp"
|
||||
#include "scripting/lua_attributes.hpp"
|
||||
#include "scripting/lua_common.hpp"
|
||||
#include "scripting/lua_cpp_function.hpp"
|
||||
#include "scripting/lua_fileops.hpp"
|
||||
|
@ -36,6 +38,7 @@
|
|||
#include "scripting/lua_preferences.hpp"
|
||||
#include "scripting/plugins/context.hpp"
|
||||
#include "scripting/plugins/manager.hpp"
|
||||
#include "scripting/push_check.hpp"
|
||||
|
||||
#ifdef DEBUG_LUA
|
||||
#include "scripting/debug_lua.hpp"
|
||||
|
@ -52,7 +55,6 @@
|
|||
|
||||
#include "lua/wrapper_lauxlib.h"
|
||||
|
||||
|
||||
static lg::log_domain log_scripting_lua("scripting/lua");
|
||||
#define DBG_LUA LOG_STREAM(debug, log_scripting_lua)
|
||||
#define LOG_LUA LOG_STREAM(info, log_scripting_lua)
|
||||
|
@ -93,6 +95,8 @@ static int intf_delay(lua_State* L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int intf_execute(lua_State* L);
|
||||
|
||||
application_lua_kernel::application_lua_kernel()
|
||||
: lua_kernel_base()
|
||||
{
|
||||
|
@ -108,9 +112,17 @@ application_lua_kernel::application_lua_kernel()
|
|||
|
||||
// Create the preferences table.
|
||||
cmd_log_ << lua_preferences::register_table(mState);
|
||||
|
||||
// Create the wesnoth.plugin table
|
||||
luaW_getglobal(mState, "wesnoth");
|
||||
lua_newtable(mState);
|
||||
lua_pushcfunction(mState, intf_execute);
|
||||
lua_setfield(mState, -2, "execute");
|
||||
lua_setfield(mState, -2, "plugin");
|
||||
lua_pop(mState, 1);
|
||||
}
|
||||
|
||||
application_lua_kernel::thread::thread(lua_State * T) : T_(T), started_(false) {}
|
||||
application_lua_kernel::thread::thread(application_lua_kernel& owner, lua_State * T) : owner_(owner), T_(T), started_(false) {}
|
||||
|
||||
std::string application_lua_kernel::thread::status()
|
||||
{
|
||||
|
@ -194,7 +206,7 @@ application_lua_kernel::thread * application_lua_kernel::load_script_from_string
|
|||
throw game::lua_error(std::string("Error when executing a script to make a lua thread -- function was not produced, found a ") + lua_typename(T, lua_type(T, -1)) );
|
||||
}
|
||||
|
||||
return new application_lua_kernel::thread(T);
|
||||
return new application_lua_kernel::thread(*this, T);
|
||||
}
|
||||
|
||||
application_lua_kernel::thread * application_lua_kernel::load_script_from_file(const std::string & file)
|
||||
|
@ -211,11 +223,12 @@ application_lua_kernel::thread * application_lua_kernel::load_script_from_file(c
|
|||
throw game::lua_error(std::string("Error when executing a file to make a lua thread -- function was not produced, found a ") + lua_typename(T, lua_type(T, -1)) );
|
||||
}
|
||||
|
||||
return new application_lua_kernel::thread(T);
|
||||
return new application_lua_kernel::thread(*this, T);
|
||||
}
|
||||
|
||||
struct lua_context_backend {
|
||||
std::vector<plugins_manager::event> requests;
|
||||
lua_kernel_base* execute;
|
||||
bool valid;
|
||||
|
||||
lua_context_backend()
|
||||
|
@ -257,6 +270,55 @@ static int impl_context_accessor(lua_State * L, std::shared_ptr<lua_context_back
|
|||
}
|
||||
}
|
||||
|
||||
extern luaW_Registry& gameConfigReg();
|
||||
static auto& dummy = gameConfigReg(); // just to ensure it's constructed.
|
||||
|
||||
GAME_CONFIG_SETTER("debug", bool, application_lua_kernel) {
|
||||
(void)k;
|
||||
game_config::set_debug(value);
|
||||
}
|
||||
|
||||
static int intf_execute(lua_State* L)
|
||||
{
|
||||
static const int CTX = 1, FUNC = 2, EVT = 3, EXEC = 4;
|
||||
if(lua_gettop(L) == 2) lua_pushnil(L);
|
||||
if(!luaW_table_get_def(L, CTX, "valid", false)) {
|
||||
lua_pushboolean(L, false);
|
||||
lua_pushstring(L, "context not valid");
|
||||
return 2;
|
||||
}
|
||||
if(!luaW_tableget(L, CTX, "execute")) {
|
||||
lua_pushboolean(L, false);
|
||||
lua_pushstring(L, "context cannot execute");
|
||||
return 2;
|
||||
}
|
||||
if(!lua_islightuserdata(L, EXEC)) {
|
||||
lua_pushboolean(L, false);
|
||||
lua_pushstring(L, "execute is not a thread");
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
config data = luaW_serialize_function(L, FUNC);
|
||||
if(data["params"] != 0) {
|
||||
lua_pushboolean(L, false);
|
||||
lua_pushstring(L, "cannot execute function with parameters");
|
||||
return 2;
|
||||
}
|
||||
if(!lua_isnil(L, EVT)) data["name"] = luaL_checkstring(L, EVT);
|
||||
lua_pushvalue(L, FUNC);
|
||||
data["ref"] = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
std::shared_ptr<lua_context_backend>* context = static_cast<std::shared_ptr<lua_context_backend>*>(lua_touserdata(L, EXEC));
|
||||
luaW_pushconfig(L, data);
|
||||
impl_context_backend(L, *context, "execute");
|
||||
} catch(luafunc_serialize_error& e) {
|
||||
lua_pushboolean(L, false);
|
||||
lua_pushstring(L, e.what());
|
||||
return 2;
|
||||
}
|
||||
lua_pushboolean(L, true);
|
||||
return 1;
|
||||
}
|
||||
bool luaW_copy_upvalues(lua_State* L, const config& cfg);
|
||||
application_lua_kernel::request_list application_lua_kernel::thread::run_script(const plugins_context & ctxt, const std::vector<plugins_manager::event> & queue)
|
||||
{
|
||||
// There are two possibilities: (1) this is the first execution, and the C function is the only thing on the stack
|
||||
|
@ -264,30 +326,37 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script(
|
|||
// Either way we push the arguments to the function and call resume.
|
||||
|
||||
// First we have to create the event table, by concatenating the event queue into a table.
|
||||
lua_newtable(T_); //this will be the event table
|
||||
for (std::size_t i = 0; i < queue.size(); ++i) {
|
||||
lua_newtable(T_);
|
||||
lua_pushstring(T_, queue[i].name.c_str());
|
||||
lua_rawseti(T_, -2, 1);
|
||||
luaW_pushconfig(T_, queue[i].data);
|
||||
lua_rawseti(T_, -2, 2);
|
||||
lua_rawseti(T_, -2, i+1);
|
||||
config events;
|
||||
for(const auto& event : queue) {
|
||||
events.add_child(event.name, event.data);
|
||||
}
|
||||
luaW_pushconfig(T_, events); //this will be the event table
|
||||
|
||||
// Now we have to create the context object. It is arranged as a table of boost functions.
|
||||
auto this_context_backend = std::make_shared<lua_context_backend>();
|
||||
lua_newtable(T_); // this will be the context table
|
||||
lua_pushstring(T_, "valid");
|
||||
lua_pushboolean(T_, true);
|
||||
lua_settable(T_, -3);
|
||||
for (const std::string & key : ctxt.callbacks_ | boost::adaptors::map_keys ) {
|
||||
lua_pushstring(T_, key.c_str());
|
||||
lua_cpp::push_function(T_, std::bind(&impl_context_backend, std::placeholders::_1, this_context_backend, key));
|
||||
lua_settable(T_, -3);
|
||||
}
|
||||
if(ctxt.execute_kernel_) {
|
||||
lua_pushstring(T_, "execute");
|
||||
lua_pushlightuserdata(T_, &this_context_backend);
|
||||
lua_settable(T_, -3);
|
||||
}
|
||||
|
||||
// Now we have to create the info object (context accessors). It is arranged as a table of boost functions.
|
||||
lua_newtable(T_); // this will be the info table
|
||||
lua_pushstring(T_, "name");
|
||||
lua_pushstring(T_, ctxt.name_.c_str());
|
||||
lua_settable(T_, -3);
|
||||
lua_pushstring(T_, "valid");
|
||||
lua_pushboolean(T_, true);
|
||||
lua_settable(T_, -3);
|
||||
for (const plugins_context::accessor_list::value_type & v : ctxt.accessors_) {
|
||||
const std::string & key = v.first;
|
||||
const plugins_context::accessor_function & func = v.second;
|
||||
|
@ -296,7 +365,14 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script(
|
|||
lua_settable(T_, -3);
|
||||
}
|
||||
|
||||
// Push copies of the context and info tables so that we can mark them invalid for the next slice
|
||||
lua_pushvalue(T_, -2);
|
||||
lua_pushvalue(T_, -2);
|
||||
// However, Lua can't handle having extra values on the stack when resuming a coroutine,
|
||||
// so move the extra copy to the main thread instead.
|
||||
lua_xmove(T_, owner_.get_state(), 2);
|
||||
// Now we resume the function, calling the coroutine with the three arguments (events, context, info).
|
||||
// We ignore any values returned via arguments to yield()
|
||||
int numres = 0;
|
||||
lua_resume(T_, nullptr, 3, &numres);
|
||||
|
||||
|
@ -328,11 +404,85 @@ application_lua_kernel::request_list application_lua_kernel::thread::run_script(
|
|||
}
|
||||
}
|
||||
|
||||
// Pop any values that the resume left on the stack
|
||||
lua_pop(T_, numres);
|
||||
// Set "valid" to false on the now-expired context and info tables
|
||||
lua_pushstring(owner_.get_state(), "valid");
|
||||
lua_pushboolean(owner_.get_state(), false);
|
||||
lua_settable(owner_.get_state(), -3);
|
||||
lua_pushstring(owner_.get_state(), "valid");
|
||||
lua_pushboolean(owner_.get_state(), false);
|
||||
lua_settable(owner_.get_state(), -4);
|
||||
lua_pop(owner_.get_state(), 2);
|
||||
|
||||
application_lua_kernel::request_list results;
|
||||
|
||||
for (const plugins_manager::event & req : this_context_backend->requests) {
|
||||
if(ctxt.execute_kernel_ && req.name == "execute") {
|
||||
results.push_back([this, lk = ctxt.execute_kernel_, data = req.data]() {
|
||||
auto result = lk->run_binary_lua_tag(data);
|
||||
int ref = result["ref"];
|
||||
if(auto func = result.optional_child("executed")) {
|
||||
lua_rawgeti(T_, LUA_REGISTRYINDEX, ref);
|
||||
luaW_copy_upvalues(T_, *func);
|
||||
luaL_unref(T_, LUA_REGISTRYINDEX, ref);
|
||||
lua_pop(T_, 1);
|
||||
}
|
||||
result.remove_children("executed");
|
||||
result.remove_attribute("ref");
|
||||
plugins_manager::get()->notify_event(result["name"], result);
|
||||
return true;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
results.push_back(std::bind(ctxt.callbacks_.find(req.name)->second, req.data));
|
||||
//results.emplace_back(ctxt.callbacks_.find(req.name)->second, req.data);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool luaW_copy_upvalues(lua_State* L, const config& cfg)
|
||||
{
|
||||
if(auto upvalues = cfg.optional_child("upvalues")) {
|
||||
lua_pushvalue(L, -1); // duplicate function because lua_getinfo will pop it
|
||||
lua_Debug info;
|
||||
lua_getinfo(L, ">u", &info);
|
||||
int funcindex = lua_absindex(L, -1);
|
||||
for(int i = 1; i <= info.nups; i++, lua_pop(L, 1)) {
|
||||
std::string_view name = lua_getupvalue(L, funcindex, i);
|
||||
if(name == "_ENV") {
|
||||
lua_pushglobaltable(L);
|
||||
} else if(upvalues->has_attribute(name)) {
|
||||
luaW_pushscalar(L, (*upvalues)[name]);
|
||||
} else if(upvalues->has_child(name)) {
|
||||
const auto& child = upvalues->mandatory_child(name);
|
||||
if(child["upvalue_type"] == "array") {
|
||||
auto children = upvalues->child_range(name);
|
||||
lua_createtable(L, children.size(), 0);
|
||||
for(const auto& cfg : children) {
|
||||
luaW_pushscalar(L, cfg["value"]);
|
||||
lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
|
||||
}
|
||||
} else if(child["upvalue_type"] == "named tuple") {
|
||||
auto children = upvalues->child_range(name);
|
||||
std::vector<std::string> names;
|
||||
for(const auto& cfg : children) {
|
||||
names.push_back(cfg["name"]);
|
||||
}
|
||||
luaW_push_namedtuple(L, names);
|
||||
for(const auto& cfg : children) {
|
||||
luaW_pushscalar(L, cfg["value"]);
|
||||
lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
|
||||
}
|
||||
} else if(child["upvalue_type"] == "config") {
|
||||
luaW_pushconfig(L, child);
|
||||
} else if(child["upvalue_type"] == "function") {
|
||||
luaW_copy_upvalues(L, child);
|
||||
lua_pushvalue(L, -1);
|
||||
}
|
||||
} else continue;
|
||||
lua_setupvalue(L, funcindex, i);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -33,13 +33,14 @@ public:
|
|||
typedef std::vector<std::function<bool(void)>> request_list;
|
||||
|
||||
class thread {
|
||||
application_lua_kernel& owner_;
|
||||
lua_State * T_;
|
||||
bool started_;
|
||||
|
||||
thread(const thread&) = delete;
|
||||
thread& operator=(const thread&) = delete;
|
||||
|
||||
thread(lua_State *);
|
||||
thread(application_lua_kernel&, lua_State *);
|
||||
public :
|
||||
bool is_running();
|
||||
std::string status();
|
||||
|
|
|
@ -569,7 +569,7 @@ namespace {
|
|||
void operator()(double d) const
|
||||
{ lua_pushnumber(L, d); }
|
||||
void operator()(const std::string& s) const
|
||||
{ lua_pushstring(L, s.c_str()); }
|
||||
{ lua_pushlstring(L, s.c_str(), s.size()); }
|
||||
void operator()(const t_string& s) const
|
||||
{ luaW_pushtstring(L, s); }
|
||||
};
|
||||
|
@ -590,7 +590,7 @@ bool luaW_toscalar(lua_State *L, int index, config::attribute_value& v)
|
|||
v = lua_tonumber(L, -1);
|
||||
break;
|
||||
case LUA_TSTRING:
|
||||
v = lua_tostring(L, -1);
|
||||
v = std::string(luaW_tostring(L, -1));
|
||||
break;
|
||||
case LUA_TUSERDATA:
|
||||
{
|
||||
|
@ -676,7 +676,7 @@ void luaW_filltable(lua_State *L, const config& cfg)
|
|||
|
||||
static int impl_namedtuple_get(lua_State* L)
|
||||
{
|
||||
if(lua_isstring(L, 2)) {
|
||||
if(lua_type(L, 2) == LUA_TSTRING) {
|
||||
std::string k = lua_tostring(L, 2);
|
||||
luaL_getmetafield(L, 1, "__names");
|
||||
auto names = lua_check<std::vector<std::string>>(L, -1);
|
||||
|
@ -690,6 +690,26 @@ static int impl_namedtuple_get(lua_State* L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int impl_namedtuple_set(lua_State* L)
|
||||
{
|
||||
if(lua_type(L, 2) == LUA_TSTRING) {
|
||||
std::string k = lua_tostring(L, 2);
|
||||
luaL_getmetafield(L, 1, "__names");
|
||||
auto names = lua_check<std::vector<std::string>>(L, -1);
|
||||
auto iter = std::find(names.begin(), names.end(), k);
|
||||
if(iter != names.end()) {
|
||||
int i = std::distance(names.begin(), iter) + 1;
|
||||
lua_pushvalue(L, 3);
|
||||
lua_rawseti(L, 1, i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// If it's not one of the special names, just assign normally
|
||||
lua_settop(L, 3);
|
||||
lua_rawset(L, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int impl_namedtuple_dir(lua_State* L)
|
||||
{
|
||||
luaL_getmetafield(L, 1, "__names");
|
||||
|
@ -709,13 +729,46 @@ static int impl_namedtuple_tostring(lua_State* L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
static int impl_namedtuple_compare(lua_State* L) {
|
||||
// Comparing a named tuple with any other table is always false.
|
||||
if(lua_type(L, 1) != LUA_TTABLE || lua_type(L, 2) != LUA_TTABLE) {
|
||||
NOT_EQUAL:
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
luaL_getmetafield(L, 1, "__name");
|
||||
luaL_getmetafield(L, 2, "__name");
|
||||
if(!lua_rawequal(L, 3, 4)) goto NOT_EQUAL;
|
||||
lua_pop(L, 2);
|
||||
// Named tuples can be equal only if they both have the exact same set of names.
|
||||
luaL_getmetafield(L, 1, "__names");
|
||||
luaL_getmetafield(L, 2, "__names");
|
||||
auto lnames = lua_check<std::vector<std::string>>(L, 3);
|
||||
auto rnames = lua_check<std::vector<std::string>>(L, 4);
|
||||
if(lnames != rnames) goto NOT_EQUAL;
|
||||
lua_pop(L, 2);
|
||||
// They are equal if all of the corresponding members in each tuple are equal.
|
||||
for(size_t i = 1; i <= lnames.size(); i++) {
|
||||
lua_rawgeti(L, 1, i);
|
||||
lua_rawgeti(L, 2, i);
|
||||
if(!lua_compare(L, 3, 4, LUA_OPEQ)) goto NOT_EQUAL;
|
||||
lua_pop(L, 2);
|
||||
}
|
||||
// Theoretically, they could have other members besides the special named ones.
|
||||
// But we ignore those for the purposes of equality.
|
||||
lua_pushboolean(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void luaW_push_namedtuple(lua_State* L, const std::vector<std::string>& names)
|
||||
{
|
||||
lua_createtable(L, names.size(), 0);
|
||||
lua_createtable(L, 0, 4);
|
||||
lua_createtable(L, 0, 8);
|
||||
static luaL_Reg callbacks[] = {
|
||||
{ "__index", &impl_namedtuple_get },
|
||||
{ "__newindex", &impl_namedtuple_set },
|
||||
{ "__dir", &impl_namedtuple_dir },
|
||||
{ "__eq", &impl_namedtuple_compare },
|
||||
{ "__tostring", &impl_namedtuple_tostring },
|
||||
{ nullptr, nullptr }
|
||||
};
|
||||
|
@ -734,9 +787,24 @@ void luaW_push_namedtuple(lua_State* L, const std::vector<std::string>& names)
|
|||
lua_setfield(L, -2, "__metatable");
|
||||
lua_push(L, names);
|
||||
lua_setfield(L, -2, "__names");
|
||||
lua_pushstring(L, "named tuple");
|
||||
lua_setfield(L, -2, "__name");
|
||||
lua_setmetatable(L, -2);
|
||||
}
|
||||
|
||||
std::vector<std::string> luaW_to_namedtuple(lua_State* L, int idx) {
|
||||
std::vector<std::string> names;
|
||||
if(luaL_getmetafield(L, idx, "__name")) {
|
||||
if(lua_check<std::string>(L, -1) == "named tuple") {
|
||||
luaL_getmetafield(L, idx, "__names");
|
||||
names = lua_check<std::vector<std::string>>(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
void luaW_pushlocation(lua_State *L, const map_location& ml)
|
||||
{
|
||||
luaW_push_namedtuple(L, {"x", "y"});
|
||||
|
@ -840,8 +908,25 @@ void luaW_pushconfig(lua_State *L, const config& cfg)
|
|||
luaW_filltable(L, cfg);
|
||||
}
|
||||
|
||||
luaW_PrintStack luaW_debugstack(lua_State* L) {
|
||||
return {L};
|
||||
}
|
||||
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const luaW_PrintStack& s) {
|
||||
int top = lua_gettop(s.L);
|
||||
os << "Lua Stack\n";
|
||||
for(int i = 1; i <= top; i++) {
|
||||
luaW_getglobal(s.L, "wesnoth", "as_text");
|
||||
lua_pushvalue(s.L, i);
|
||||
lua_call(s.L, 1, 1);
|
||||
auto value = luaL_checkstring(s.L, -1);
|
||||
lua_pop(s.L, 1);
|
||||
os << '[' << i << ']' << value << '\n';
|
||||
}
|
||||
if(top == 0) os << "(empty)\n";
|
||||
os << std::flush;
|
||||
return os;
|
||||
}
|
||||
|
||||
#define return_misformed() \
|
||||
do { lua_settop(L, initial_top); return false; } while (0)
|
||||
|
@ -1041,7 +1126,7 @@ bool luaW_checkvariable(lua_State *L, variable_access_create& v, int n)
|
|||
v.as_scalar() = lua_tonumber(L, n);
|
||||
return true;
|
||||
case LUA_TSTRING:
|
||||
v.as_scalar() = lua_tostring(L, n);
|
||||
v.as_scalar() = std::string(luaW_tostring(L, n));
|
||||
return true;
|
||||
case LUA_TUSERDATA:
|
||||
if (t_string * t_str = static_cast<t_string*> (luaL_testudata(L, n, tstringKey))) {
|
||||
|
|
|
@ -101,6 +101,12 @@ void luaW_filltable(lua_State *L, const config& cfg);
|
|||
*/
|
||||
void luaW_push_namedtuple(lua_State* L, const std::vector<std::string>& names);
|
||||
|
||||
/**
|
||||
* Get the keys of a "named tuple" from the stack.
|
||||
* Returns an empty array if the stack element is not a named tuple.
|
||||
*/
|
||||
std::vector<std::string> luaW_to_namedtuple(lua_State* L, int idx);
|
||||
|
||||
/**
|
||||
* Converts a map location object to a Lua table pushed at the top of the stack.
|
||||
*/
|
||||
|
@ -225,6 +231,10 @@ int luaW_pcall_internal(lua_State *L, int nArgs, int nRets);
|
|||
int luaW_type_error(lua_State *L, int narg, const char *tname);
|
||||
int luaW_type_error(lua_State *L, int narg, const char* kpath, const char *tname);
|
||||
|
||||
struct luaW_PrintStack { lua_State* L; };
|
||||
luaW_PrintStack luaW_debugstack(lua_State* L);
|
||||
std::ostream& operator<<(std::ostream& os, const luaW_PrintStack&);
|
||||
|
||||
#define deprecate_attrib(name, prefix, level, version, msg) deprecated_message(prefix "." name, DEP_LEVEL::level, version, msg)
|
||||
|
||||
#define return_deprecated_attrib(type_macro, name, accessor, prefix, level, version, msg) \
|
||||
|
|
|
@ -1065,10 +1065,10 @@ bool lua_kernel_base::protected_call(lua_State * L, int nArgs, int nRets, error_
|
|||
return true;
|
||||
}
|
||||
|
||||
bool lua_kernel_base::load_string(char const * prog, const std::string& name, error_handler e_h)
|
||||
bool lua_kernel_base::load_string(const std::string& prog, const std::string& name, error_handler e_h, bool allow_unsafe)
|
||||
{
|
||||
// pass 't' to prevent loading bytecode which is unsafe and can be used to escape the sandbox.
|
||||
int errcode = luaL_loadbufferx(mState, prog, strlen(prog), name.empty() ? prog : name.c_str(), "t");
|
||||
int errcode = luaL_loadbufferx(mState, prog.c_str(), prog.size(), name.empty() ? prog.c_str() : name.c_str(), allow_unsafe ? "tb" : "t");
|
||||
if (errcode != LUA_OK) {
|
||||
char const * msg = lua_tostring(mState, -1);
|
||||
std::string message = msg ? msg : "null string";
|
||||
|
@ -1101,6 +1101,169 @@ void lua_kernel_base::run_lua_tag(const config& cfg)
|
|||
}
|
||||
this->run(cfg["code"].str().c_str(), cfg["name"].str(), nArgs);
|
||||
}
|
||||
|
||||
config luaW_serialize_function(lua_State* L, int func)
|
||||
{
|
||||
if(lua_iscfunction(L, func)) {
|
||||
throw luafunc_serialize_error("cannot serialize C function");
|
||||
}
|
||||
if(!lua_isfunction(L, func)) {
|
||||
throw luafunc_serialize_error("cannot serialize callable non-function");
|
||||
}
|
||||
config data;
|
||||
lua_Debug info;
|
||||
lua_pushvalue(L, func); // push copy of function because lua_getinfo will pop it
|
||||
lua_getinfo(L, ">u", &info);
|
||||
data["params"] = info.nparams;
|
||||
luaW_getglobal(L, "string", "dump");
|
||||
lua_pushvalue(L, func);
|
||||
lua_call(L, 1, 1);
|
||||
data["code"] = lua_check<std::string>(L, -1);
|
||||
lua_pop(L, 1);
|
||||
config upvalues;
|
||||
for(int i = 1; i <= info.nups; i++, lua_pop(L, 1)) {
|
||||
std::string_view name = lua_getupvalue(L, func, i);
|
||||
if(name == "_ENV") {
|
||||
upvalues.add_child(name)["upvalue_type"] = "_ENV";
|
||||
continue;
|
||||
}
|
||||
int idx = lua_absindex(L, -1);
|
||||
switch(lua_type(L, idx)) {
|
||||
case LUA_TBOOLEAN: case LUA_TNUMBER: case LUA_TSTRING:
|
||||
luaW_toscalar(L, idx, upvalues[name]);
|
||||
break;
|
||||
case LUA_TFUNCTION:
|
||||
upvalues.add_child(name, luaW_serialize_function(L, idx))["upvalue_type"] = "function";
|
||||
break;
|
||||
case LUA_TNIL:
|
||||
upvalues.add_child(name, config{"upvalue_type", "nil"});
|
||||
break;
|
||||
case LUA_TTABLE:
|
||||
if(std::vector<std::string> names = luaW_to_namedtuple(L, idx); !names.empty()) {
|
||||
for(size_t i = 1; i <= lua_rawlen(L, -1); i++, lua_pop(L, 1)) {
|
||||
lua_rawgeti(L, idx, i);
|
||||
config& cfg = upvalues.add_child(name);
|
||||
luaW_toscalar(L, -1, cfg["value"]);
|
||||
cfg["name"] = names[0];
|
||||
cfg["upvalue_type"] = "named tuple";
|
||||
names.erase(names.begin());
|
||||
}
|
||||
break;
|
||||
} else if(config cfg; luaW_toconfig(L, idx, cfg)) {
|
||||
std::vector<std::string> names;
|
||||
int save_top = lua_gettop(L);
|
||||
if(luaL_getmetafield(L, idx, "__name") && lua_check<std::string>(L, -1) == "named tuple") {
|
||||
luaL_getmetafield(L, -2, "__names");
|
||||
names = lua_check<std::vector<std::string>>(L, -1);
|
||||
}
|
||||
lua_settop(L, save_top);
|
||||
upvalues.add_child(name, cfg)["upvalue_type"] = names.empty() ? "config" : "named tuple";
|
||||
break;
|
||||
} else {
|
||||
for(size_t i = 1; i <= lua_rawlen(L, -1); i++, lua_pop(L, 1)) {
|
||||
lua_rawgeti(L, idx, i);
|
||||
config& cfg = upvalues.add_child(name);
|
||||
luaW_toscalar(L, -1, cfg["value"]);
|
||||
cfg["upvalue_type"] = "array";
|
||||
}
|
||||
bool found_non_array = false;
|
||||
for(lua_pushnil(L); lua_next(L, idx); lua_pop(L, 1)) {
|
||||
if(lua_type(L, -2) != LUA_TNUMBER) {
|
||||
found_non_array = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found_non_array) break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
std::ostringstream os;
|
||||
os << "cannot serialize function with upvalue " << name << " = ";
|
||||
luaW_getglobal(L, "wesnoth", "as_text");
|
||||
lua_pushvalue(L, idx);
|
||||
lua_call(L, 1, 1);
|
||||
os << luaL_checkstring(L, -1);
|
||||
lua_pushboolean(L, false);
|
||||
throw luafunc_serialize_error(os.str());
|
||||
}
|
||||
}
|
||||
if(!upvalues.empty()) data.add_child("upvalues", upvalues);
|
||||
return data;
|
||||
}
|
||||
|
||||
bool lua_kernel_base::load_binary(const config& cfg, error_handler eh)
|
||||
{
|
||||
if(!load_string(cfg["code"].str(), cfg["name"], eh, true)) return false;
|
||||
if(auto upvalues = cfg.optional_child("upvalues")) {
|
||||
lua_pushvalue(mState, -1); // duplicate function because lua_getinfo will pop it
|
||||
lua_Debug info;
|
||||
lua_getinfo(mState, ">u", &info);
|
||||
int funcindex = lua_absindex(mState, -1);
|
||||
for(int i = 1; i <= info.nups; i++) {
|
||||
std::string_view name = lua_getupvalue(mState, funcindex, i);
|
||||
lua_pop(mState, 1); // we only want the upvalue's name, not its value
|
||||
if(name == "_ENV") {
|
||||
lua_pushglobaltable(mState);
|
||||
} else if(upvalues->has_attribute(name)) {
|
||||
luaW_pushscalar(mState, (*upvalues)[name]);
|
||||
} else if(upvalues->has_child(name)) {
|
||||
const auto& child = upvalues->mandatory_child(name);
|
||||
if(child["upvalue_type"] == "array") {
|
||||
auto children = upvalues->child_range(name);
|
||||
lua_createtable(mState, children.size(), 0);
|
||||
for(const auto& cfg : children) {
|
||||
luaW_pushscalar(mState, cfg["value"]);
|
||||
lua_rawseti(mState, -2, lua_rawlen(mState, -2) + 1);
|
||||
}
|
||||
} else if(child["upvalue_type"] == "config") {
|
||||
luaW_pushconfig(mState, child);
|
||||
} else if(child["upvalue_type"] == "function") {
|
||||
if(!load_binary(child, eh)) return false;
|
||||
} else if(child["upvalue_type"] == "nil") {
|
||||
lua_pushnil(mState);
|
||||
}
|
||||
} else continue;
|
||||
lua_setupvalue(mState, funcindex, i);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
config lua_kernel_base::run_binary_lua_tag(const config& cfg)
|
||||
{
|
||||
int top = lua_gettop(mState);
|
||||
try {
|
||||
error_handler eh = std::bind(&lua_kernel_base::throw_exception, this, std::placeholders::_1, std::placeholders::_2 );
|
||||
if(load_binary(cfg, eh)) {
|
||||
lua_pushvalue(mState, -1);
|
||||
protected_call(0, LUA_MULTRET, eh);
|
||||
}
|
||||
} catch (const game::lua_error & e) {
|
||||
cmd_log_ << e.what() << "\n";
|
||||
lua_kernel_base::log_error(e.what(), "In function lua_kernel::run()");
|
||||
config error;
|
||||
error["name"] = "execute_error";
|
||||
error["error"] = e.what();
|
||||
return error;
|
||||
}
|
||||
config result;
|
||||
result["ref"] = cfg["ref"];
|
||||
result.add_child("executed") = luaW_serialize_function(mState, top + 1);
|
||||
lua_remove(mState, top + 1);
|
||||
result["name"] = "execute_result";
|
||||
for(int i = top + 1; i < lua_gettop(mState); i++) {
|
||||
std::string index = std::to_string(i - top);
|
||||
switch(lua_type(mState, i)) {
|
||||
case LUA_TNUMBER: case LUA_TBOOLEAN: case LUA_TSTRING:
|
||||
luaW_toscalar(mState, i, result[index]);
|
||||
break;
|
||||
case LUA_TTABLE:
|
||||
luaW_toconfig(mState, i, result.add_child(index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Call load_string and protected call. Make them throw exceptions.
|
||||
//
|
||||
void lua_kernel_base::throwing_run(const char * prog, const std::string& name, int nArgs, bool in_interpreter)
|
||||
|
|
|
@ -32,6 +32,9 @@ public:
|
|||
/** Runs a [lua] tag. Doesn't throw lua_error.*/
|
||||
void run_lua_tag(const config& cfg);
|
||||
|
||||
/** Runs a binary [lua] tag. Doesn't throw lua_error.*/
|
||||
config run_binary_lua_tag(const config& cfg);
|
||||
|
||||
/** Runs a plain script. Doesn't throw lua_error.*/
|
||||
void run(char const *prog, const std::string& name, int nArgs = 0);
|
||||
|
||||
|
@ -125,7 +128,8 @@ protected:
|
|||
// Execute a protected call, taking a lua_State as argument. For functions pushed into the lua environment, this version should be used, or the function cannot be used by coroutines without segfaulting (since they have a different lua_State pointer). This version is called by the above version.
|
||||
static bool protected_call(lua_State * L, int nArgs, int nRets, error_handler);
|
||||
// Load a string onto the stack as a function. Returns true if successful, error handler is called if not.
|
||||
bool load_string(char const * prog, const std::string& name, error_handler);
|
||||
bool load_string(const std::string& prog, const std::string& name, error_handler, bool allow_unsafe = false);
|
||||
bool load_binary(const config& func, error_handler);
|
||||
|
||||
virtual bool protected_call(int nArgs, int nRets); // select default error handler polymorphically
|
||||
virtual bool load_string(char const * prog, const std::string& name); // select default error handler polymorphically
|
||||
|
@ -146,6 +150,12 @@ private:
|
|||
std::vector<std::tuple<std::string, std::string>> registered_widget_definitions_;
|
||||
};
|
||||
|
||||
config luaW_serialize_function(lua_State* L, int func);
|
||||
|
||||
struct luafunc_serialize_error : public std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
std::vector<std::string> luaW_get_attributes(lua_State* L, int idx);
|
||||
|
||||
struct game_config_tag {
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace lua_preferences
|
|||
lua_setfield(L, -2, "__newindex");
|
||||
lua_pushcfunction(L, impl_preferences_dir);
|
||||
lua_setfield(L, -2, "__dir");
|
||||
lua_pushstring(L, "src/scripting/lua_preferences.cpp");
|
||||
lua_pushstring(L, "preferences");
|
||||
lua_setfield(L, -2, "__metatable");
|
||||
|
||||
// Set the table as its own metatable
|
||||
|
|
|
@ -23,7 +23,18 @@ class enable_lua_ptr
|
|||
{
|
||||
public:
|
||||
enable_lua_ptr(T* tp) : self_(std::make_shared<T*>(tp)) {}
|
||||
enable_lua_ptr(enable_lua_ptr&& o) : self_(std::move(o.self_))
|
||||
{
|
||||
*self_ = static_cast<T*>(this);
|
||||
}
|
||||
enable_lua_ptr& operator=(enable_lua_ptr&& o)
|
||||
{
|
||||
self_ = std::move(o.self_);
|
||||
*self_ = static_cast<T*>(this);
|
||||
}
|
||||
private:
|
||||
enable_lua_ptr(const enable_lua_ptr& o) = delete;
|
||||
enable_lua_ptr& operator=(const enable_lua_ptr& o) = delete;
|
||||
friend class lua_ptr<T>;
|
||||
std::shared_ptr<T*> self_;
|
||||
};
|
||||
|
@ -41,5 +52,17 @@ public:
|
|||
}
|
||||
return nullptr;
|
||||
}
|
||||
T* operator->()
|
||||
{
|
||||
return get_ptr();
|
||||
}
|
||||
operator bool() const
|
||||
{
|
||||
return bool(self_.lock());
|
||||
}
|
||||
bool operator!() const
|
||||
{
|
||||
return !operator bool();
|
||||
}
|
||||
std::weak_ptr<T*> self_;
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include "scripting/plugins/context.hpp"
|
||||
|
||||
#include "scripting/plugins/manager.hpp"
|
||||
#include "scripting/lua_kernel_base.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
|
@ -80,6 +81,11 @@ void plugins_context::set_accessor_int(const std::string & name, std::function<i
|
|||
set_accessor(name, [func, name](const config& cfg) { return config {name, func(cfg)}; });
|
||||
}
|
||||
|
||||
void plugins_context::set_accessor_bool(const std::string & name, std::function<bool(config)> func)
|
||||
{
|
||||
set_accessor(name, [func, name](const config& cfg) { return config {name, func(cfg)}; });
|
||||
}
|
||||
|
||||
|
||||
std::size_t plugins_context::erase_accessor(const std::string & name)
|
||||
{
|
||||
|
@ -103,3 +109,7 @@ void plugins_context::set_callback(const std::string & name, std::function<void(
|
|||
{
|
||||
set_callback(name, [func, preserves_context](config cfg) { func(cfg); return preserves_context; });
|
||||
}
|
||||
|
||||
void plugins_context::set_callback_execute(lua_kernel_base& kernel) {
|
||||
execute_kernel_ = &kernel;
|
||||
}
|
||||
|
|
|
@ -47,12 +47,14 @@ public:
|
|||
|
||||
void set_callback(const std::string & name, callback_function);
|
||||
void set_callback(const std::string & name, std::function<void(config)> function, bool preserves_context);
|
||||
void set_callback_execute(class lua_kernel_base& kernel);
|
||||
std::size_t erase_callback(const std::string & name);
|
||||
std::size_t clear_callbacks();
|
||||
|
||||
void set_accessor(const std::string & name, accessor_function);
|
||||
void set_accessor_string(const std::string & name, std::function<std::string(config)>); //helpers which create a config from a simple type
|
||||
void set_accessor_int(const std::string & name, std::function<int(config)>);
|
||||
void set_accessor_bool(const std::string & name, std::function<bool(config)>);
|
||||
std::size_t erase_accessor(const std::string & name);
|
||||
std::size_t clear_accessors();
|
||||
|
||||
|
@ -67,4 +69,5 @@ private:
|
|||
callback_list callbacks_;
|
||||
accessor_list accessors_;
|
||||
std::string name_;
|
||||
lua_kernel_base* execute_kernel_;
|
||||
};
|
||||
|
|
|
@ -93,13 +93,13 @@ namespace lua_check_impl
|
|||
std::enable_if_t<std::is_same_v<T, std::string>, std::string>
|
||||
lua_check(lua_State *L, int n)
|
||||
{
|
||||
return luaL_checkstring(L, n);
|
||||
return std::string(luaW_tostring(L, n));
|
||||
}
|
||||
template<typename T>
|
||||
std::enable_if_t<std::is_same_v<T, std::string>, std::string>
|
||||
lua_to_or_default(lua_State *L, int n, const T& def)
|
||||
{
|
||||
return luaL_optstring(L, n, def.c_str());
|
||||
return std::string(luaW_tostring_or_default(L, n, def));
|
||||
}
|
||||
template<typename T>
|
||||
std::enable_if_t<std::is_same_v<T, std::string>, void>
|
||||
|
|
43
src/tests/test_lua_ptr.cpp
Normal file
43
src/tests/test_lua_ptr.cpp
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Copyright (C) 2024 - 2024
|
||||
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY.
|
||||
|
||||
See the COPYING file for more details.
|
||||
COPYING file for more details.
|
||||
*/
|
||||
|
||||
#define GETTEXT_DOMAIN "wesnoth-test"
|
||||
|
||||
#include "scripting/lua_ptr.hpp"
|
||||
#include <boost/test/unit_test.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct dummy_object : public enable_lua_ptr<dummy_object> {
|
||||
std::string value;
|
||||
dummy_object(const std::string& s) : enable_lua_ptr<dummy_object>(this), value(s) {}
|
||||
};
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_lua_ptr) {
|
||||
std::vector<dummy_object> vec;
|
||||
auto& obj = vec.emplace_back("test");
|
||||
BOOST_CHECK_EQUAL(obj.value, "test");
|
||||
lua_ptr<dummy_object> ptr(obj);
|
||||
BOOST_CHECK(ptr);
|
||||
BOOST_CHECK_EQUAL(ptr.get_ptr(), &obj);
|
||||
{
|
||||
auto obj_moved = std::move(obj);
|
||||
BOOST_CHECK(ptr);
|
||||
BOOST_CHECK_EQUAL(ptr.get_ptr(), &obj_moved);
|
||||
BOOST_CHECK_EQUAL(ptr->value, "test");
|
||||
vec.clear();
|
||||
}
|
||||
BOOST_CHECK(!ptr);
|
||||
}
|
|
@ -762,6 +762,8 @@ static int do_gameloop(commandline_options& cmdline_opts)
|
|||
|
||||
const plugins_context::reg_vec callbacks {
|
||||
{"play_multiplayer", std::bind(&game_launcher::play_multiplayer, game.get(), game_launcher::mp_mode::CONNECT)},
|
||||
{"play_local", std::bind(&game_launcher::play_multiplayer, game.get(), game_launcher::mp_mode::LOCAL)},
|
||||
{"play_campaign", std::bind(&game_launcher::play_campaign, game.get())},
|
||||
};
|
||||
|
||||
const plugins_context::areg_vec accessors {
|
||||
|
|
23
utils/emmylua/plugin.lua
Normal file
23
utils/emmylua/plugin.lua
Normal file
|
@ -0,0 +1,23 @@
|
|||
---@meta
|
||||
|
||||
---@alias plugin_callback fun(data:WMLTable)
|
||||
---@alias plugin_accessor fun(query?:WMLTable):string|integer|WMLTable
|
||||
|
||||
---Contains mutators for the current context.
|
||||
---@class plugin_context
|
||||
---@field [string] plugin_callback A mutator takes a WML table as its only argument.
|
||||
|
||||
---Contains accessors for the current context
|
||||
---@class plugin_info
|
||||
---@field name string The name of the current context.
|
||||
---@field [string] plugin_accessor An accessor takes a WML table as its argument and returns a string, integer, or WML table.
|
||||
|
||||
---Execute a function within the current context's game state, if supported.
|
||||
---Functions returning a value can request that the result be returned in an event in the next slice.
|
||||
---If the function raises an error, that too will be returned as an event in the next slice.
|
||||
---@param context plugin_context The current plugin context.
|
||||
---@param fcn function An arbitrary function to execute. The function will be run in a different Lua kernel and thus cannot access the wesnoth.plugin module.
|
||||
---@param event_name? string The name to use for the event that contains the function's result
|
||||
---@return boolean #True if the function will be executed; false if unsupported
|
||||
---@return string? #If the first value is false, this will hold an explanatory string
|
||||
function wesnoth.plugin.execute(context, fcn, event_name) end
|
Loading…
Add table
Reference in a new issue