Compare commits

...
Sign in to create a new pull request.

22 commits

Author SHA1 Message Date
Celtic Minstrel
91c7536789 Add --plugin option to the Xcode scheme 2024-10-07 08:42:59 -04:00
Celtic Minstrel
7d77acaccc Lua API: Delete the --script command-line option
This is basically superceded by --plugin
Though it's supposed to be a script that can control the client, that capability doesn't actually exist in the implementation.
2024-10-07 08:42:59 -04:00
Celtic Minstrel
4ce8e19697 Add a sample Lua plugin that starts TSG, plays one turn, and then exits. 2024-10-07 08:42:43 -04:00
Celtic Minstrel
c746e38cc9 Lua API: Add synced_command and end_turn actions to the Game plugin context 2024-10-07 08:42:42 -04:00
Celtic Minstrel
f793d8ef7e Lua API: Plugin execute functionality now supports named tuples in the closure 2024-10-07 08:42:42 -04:00
Celtic Minstrel
15062976ca Lua API: Some enhancements/fixes to named tuples.
* Make named tuples comparable with == and ~=
* Fix the possibility of the numeric and string keys becoming out-of-syn on assignment
* Fix non-existent numeric keys being treated like a string key
2024-10-07 08:42:42 -04:00
Celtic Minstrel
ca301e1745 Lua API: ai.get_suitable_keep now returns integers instead of real numbers 2024-10-07 08:42:42 -04:00
Celtic Minstrel
40625ab26f Lua API: Make wesnoth.game_config.debug writable in plugins 2024-10-07 08:42:42 -04:00
Celtic Minstrel
3387eb2c26 Lua API: Plugins can now execute gamestate code once they are in the game context. 2024-10-07 08:42:42 -04:00
Celtic Minstrel
cc1069734c Add a debugging function that prints out the contents of the Lua stack 2024-10-07 08:42:41 -04:00
Celtic Minstrel
744cdb2085 Lua API: Correct __metatable for wesnoth.preferences 2024-10-07 08:42:41 -04:00
Celtic Minstrel
802869bf23 Lua API: Fix some places that weren't handling strings correctly.
Lua strings can contain nulls. The affected places would truncate such strings.
2024-10-07 08:42:41 -04:00
Celtic Minstrel
7e912d532f Lua API: Add a valid key to the plugin context and info tables
Previously the only way to tell they were valid would be a protected call on an existing member.
2024-10-07 08:42:41 -04:00
Celtic Minstrel
87b6cf80e8 mp_join_game should not be skippable 2024-10-07 08:42:40 -04:00
Celtic Minstrel
083c49f916 WIP 2024-10-07 08:42:40 -04:00
Celtic Minstrel
e20d054f7b WIP: Output the ID of the dialog being skipped (maybe it's not actually a leader select dialog?) 2024-10-07 08:42:40 -04:00
Celtic Minstrel
42991138d9 Make enable_lua_ptr movable and non-copyable 2024-10-07 08:42:40 -04:00
Celtic Minstrel
1b4fe1b61b Update host.lua and join.lua in light of recent changes
* Use the wesnoth.plugin module
* Use .tag and .contents instead of [1] and [2]
* Remove superfluous parentheses on function calls
2024-10-07 08:42:40 -04:00
Celtic Minstrel
68105ed4cc Lua API: Add a new plugin module with some useful functions for plugins 2024-10-07 08:42:40 -04:00
Celtic Minstrel
f971705548 Lua API: The plugin events list is now a proper WML table, with the tags being named tuples 2024-10-07 08:42:40 -04:00
Celtic Minstrel
b9d8851338 Lua API: Avoid looking up the requested scenario twice 2024-10-07 08:42:39 -04:00
Celtic Minstrel
a1c78edda5 Lua API: Enable plugins to play a campaign
This also fixes some issues that can prevent a plugin from working with the GUI (that is, only working in headless mode). Plugins should work in both headless and GUI mode.
2024-10-07 08:42:39 -04:00
38 changed files with 1009 additions and 191 deletions

81
data/lua/core/plugin.lua Normal file
View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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" />

View file

@ -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;
};

View file

@ -55,6 +55,10 @@
argument = "--no-log-to-file"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--plugin=&quot;$(PROJECT_DIR)/../../data/test/plugin/start-campaign.lua&quot;"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--data-dir=&quot;$(PROJECT_DIR)/../..&quot;"
isEnabled = "NO">

View file

@ -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

View file

@ -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;
}
}

View file

@ -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"))

View file

@ -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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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)

View file

@ -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 ***** ****** *****/

View file

@ -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()

View file

@ -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();

View file

@ -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),
};
});

View file

@ -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()

View file

@ -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()

View file

@ -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);
});
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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();

View file

@ -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))) {

View file

@ -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) \

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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_;
};

View file

@ -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;
}

View file

@ -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_;
};

View file

@ -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>

View 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);
}

View file

@ -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
View 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