The main idea here is to provide a safe and flexible way to extend Sider functionality. It allows modders to alter many aspects of game logic, and load all sorts of extra content. Visual and gameplay tweaks are possible too.
Instead of offering a C interface, where the programmers would need to write a DLL, i'm a taking a different approach here and trying to design a system, where extension modules are written in Lua. A module would typically initialize itself by registering for certain events, and then Sider will call the module functions, when those events occur later in the game.
Lua is by now a well-established language of choice for scripting support in games, used all over the gaming industry. Most famous example is probably WarCraft, but many modern games use it, including Pro Evolution Soccer itself. To boost the performance, Sider uses a just-in-time compiler for Lua called LuaJIT (https://luajit.org), written by Mike Pall. LuaJIT is a truly brilliant piece of software. It is 100% compatible with Lua 5.1, and it is super fast, often approaching and sometimes exceeding the speed of C code.
After reading this guide, the next step is to study the example (and non-example) modules, which are provided with this release of Sider. Find them in the "modules" directory.
TopIf you are familiar with Lua and how modules are typically organized then this will make full sense to you. If are you new to Lua, i would strongly recommend reading "Programming in Lua" by Roberto Ierusalimschy. 2nd edition covers Lua 5.1, which is the version of the language used by Sider. However, any other edition of the book will be just as helpful.
In any case, the module organization is pretty simple:
local m = {} function m.init(ctx) log("Hello, world!") end return m
As you have already guessed, this module doesn't really do much. But it is a valid module, and can be loaded by Sider. For that you need to save it as {something}.lua file in the "modules" folder, inside sider. Let's assume that you named it: test.lua. Then, you must also enable it in sider.ini, like this:
lua.module = "test.lua"
If you now run the game, your module will get loaded by Sider, and then the "init" function will be called, so you should see a "Hello, world!" message in sider.log. If you made a mistake and your module has a syntax error, for example, or some other problem, don't panic, instead open sider.log with Notepad (or any text editor) and look for an error message there. It will explain where (on which line of your script) and what sort of problem occurred.
VERY IMPORTANT NOTE:
File encoding must be UTF-8. This is vital, if you are
using non-latin characters in the strings in the module code - for example,
in paths. If you only have latin-1 chars, then ANSI is ok too.
Let's now see how you can make a more useful module. First step for that is to understand the context object (ctx).
TopContext object is what Sider knows about the current state of the game. As more scripts/modules are created by the community the context object will probably also change, and include more and more information about the game.
As of 4.2.0 release, the context object has the following:
ctx.home_team | id of the home team of the current match, after it is selected in the exhibition match, or the next match is determined in the league mode. |
---|---|
ctx.away_team | id of the away team of the current match, after it is selected in the exhibition match, or the next match is determined in the league mode. |
ctx.tournament_id | numeric id of the current tournament. See doc/tournament.txt file for the list of ids for all of the game's tournaments. |
ctx.match_id | some sort of number, which seems to indicate where in the season you currently are. However, the exact meaning of this id is unclear. Maybe you can figure it out and tell me ;-) |
ctx.match_leg | set to 1 or 2, if this is 1st leg or 2nd leg of a two-legged knock-out match. |
ctx.match_info | meaning of this field varies, depending on the type of tournament:
|
ctx.stadium | numeric id of the stadium that will be used (or was last used, if the stadium for the next match hasn't been chosen yet by the game logic) |
ctx.stadium_choice | This field is set when the game is in one of the
exhibition modes, and the stadium is allowed to
be selected via the menu. Once the actual final
selection of the stadium is made (right after the
"set_stadium" event fires), the "stadium_choice"
field get removed from the context. Special values:
|
ctx.timeofday | The values are: 0 - day, 1 - night. Same as with stadium, it represents the time of day of the coming match, or the last one, if the stadium isn't set yet for the next match. |
ctx.season | The values are: 0 - summer, 1 - winter. |
ctx.weather | The values are: 0 - fine, 1 - rainy, 2 - snowy |
ctx.weather_effects | The known values are:
|
ctx.match_time | Duration of the match in minutes. |
ctx.kits | table of functions for kit manipulation. (See more details further down) |
The context object also contains a register function, which has the following signature:
ctx.register(event_name, handler_function)
The event_name parameter is a string, which identifies a particular event. The handler_function should be a function in your Lua module, which will be called when the corresponding event happens. Parameters vary depending on the event - see the next section for details on all supported events and their handler signatures
The idea of the context object is that in your handler functions, your code will make decisions on what to do, using the information in the context.
It is a table of helper functions to manipulate kits (uniforms).
NOTE:
kit operations work for both "licensed" and "unlicensed" teams
The table contains the following functions:
ctx.kits.get_current_team(home_or_away)
Returns the team id for the currently chosen home or away team - depending on the parameter that you pass in:
ctx.kits.get_current_kit_id(home_or_away)
Returns the currently chosen player kit_id for home or away team - depending on the parameter that you pass in:
ctx.kits.get(team_id, kit_id)
Returns a table of attributes for a player kit for specified team and kit_id. The kit_id can range from 0 to 9. The returned table contains all attributes for the requested kit - things like: Collar, KitModel, KitFile, BackNumbersFile, and so for. We call this collection of attributes a "kit config".
ctx.kits.set(team_id, kit_id, kit_cfg, home_or_away)
Sets the kit attributes to the values provided in kit_cfg table, for the chosen team and kit. The last parameter is optional: if you specify it, then use 0 - for home team, 1 - for away team, and sider will update parts of team info in memory that control the color of radar. This allows for correct radar colors, and also for proper color-matching, when kits are initially chosen by the game logic, in such a way that they don't clash.
ctx.kits.get_gk(team_id)
Returns a table of attributes for a goalkeeper kit for specified team The returned table contains all attributes for the kit, like it is with a player kit.
ctx.kits.set_gk(team_id, kit_cfg)
Sets the kit attributes to the values provided in kit_cfg table, for the chosen team and goalkeeper kit.
ctx.kits.refresh(home_or_away)
Forces a refresh of the kit for either home or away team - depending on the value of the parameter:
Event name: "livecpk_make_key"
Handler function: f(ctx, filename)
Return value is expected to be a string or nil.
This event occurs when the game needs to find out some information about the file, specified by relative filename. Maybe the game needs to create a buffer, or it needs to determine the filesize, or something else. Your handler function is expected to return a string key that will be used for caching of "livecpk_get_filepath". You can just return filename as the key, without any changes, or return nil - which will result in the same outcome. But sometimes, you need to make a different key, because the context has some additional information that is important.
Event name: "livecpk_get_filepath"
Handler function: f(ctx, filename, key)
Return value is expected to be a string or nil.
This event is related to "livecpk_make_key". It works like this: after the key is returned by livecpk_make_key handler, sider needs to know which actual file (absolute path) needs to be loaded for this key. So your handler function is expected to return a full absolute filename. Sider will cache this absolute filename using the key returned by livecpk_make_key, and the next time this file is needed, the livecpk_get_filepath event will NOT fire. (This is again done for performance reasons so that we don't unnecessarily seek the disk).
Event name: "livecpk_rewrite"
Handler function: f(ctx, filename)
Return value is expected to be a string or nil.
Allows the filename to be rewritten to another. This is a very powerful, but also quite dangerous function, if you are not careful. If you rewrite the filename to something that does not exist in LiveCPK roots or in download/data CPKs, and none of your modules provide the content, then the game will be unable to load the file, which can lead to different behaviours, depending on type of file being loaded. If it's a DDS texture, then usually nothing bad happens - you just get a white texture. But if it is a model file - you will get an endless loop, where loading indicator will keep spinning forever, or the game can just crash. So, be careful, and don't rewrite blindly ;-)
See "kitrewrite.lua" module for an example of rewrite usage: it is loading a 2nd player kit instead of a 1st goalkeeper kit, so your goalkeepers end up wearing 2nd kit of outfield players.
Event name: "livecpk_data_ready"
Handler function: f(ctx, filename, data, size, total_size, offset, cpk_filename)
Return value: nil.
Provides a pointer to the buffer in memory that have just been filled with data. Here is what the parameters mean:
See filedump.lua, tracer.lua and facedump.lua modules for example usage of this event. This is an advanced feature, which you will need if you wanted to examine the data that the game is about to use. Care must be taken to not leak memory. Also, be aware that some large files are not read all-at-once. For instance, music from BGM.awb is accessed in small chunks - typically 0x8000 bytes in size, which is enough for continuous playback of the song. In such situation, you can use offset and total_size parameters to check what part of the file was read.
Another possible use for this event, is tracking of what is happening in the game. For example, when match is paused, a particular JSON file is loaded, and then when the match resumes, another JSON file is loaded by the game. You can track and use that information in your Lua scripts to trigger some additional logic, for example, to play a sound file, or do some other task
Here is an example module that uses "livecpk_data_ready" event to log every file as it is loaded by the game:
local m = {} function m.data_ready(ctx, filename, data, size, total_size, offset, cpk_filename) log(filename) end function m.init(ctx) ctx.register("livecpk_data_ready", m.data_ready) end return m
Event name: "set_teams"
Handler function: f(ctx, home_team, away_team)
Return value expected: nil
This event fires after both home and away teams are determined - either during the team selection in exhibition game, or when the next match becomes known in a league or a cup mode (League, Master League, UCL, Europa League, etc.) The team ids are also set as "home_team" and "away_team" fields in the context object so that they can be used later, if needed.
Event name: "set_kits"
Handler function: f(ctx, home_kit_info, away_kit_info)
Return value expected: nil or table or two tables.
This event fires when player kits are chosen for both home and away teams - The "home_kit_info" and "away_kit_info" contain kit attributes for home and away player kits respectively. The handler function can modify the kit attributes at this point by returning tables - one for home team, another - for away team, containing the changed attributes. (For more information on this, consult the "Kitserver 2020" thread on EvoWeb forums)
Event name: "set_home_team_for_kits"
Handler function: f(ctx, team_id, is_edit_mode)
Return value expected: nil.
This event fires when a home team is selected for loading of kits which happens soon after. The 3rd parameter (boolean) indicates whether we are in Edit Mode or not.
Event name: "set_away_team_for_kits"
Handler function: f(ctx, team_id, is_edit_mode)
Return value expected: nil.
This event fires when an away team is selected for loading of kits which happens soon after.
The 3rd parameter (boolean) indicates whether we are in Edit Mode or not.
(NOTE: the "is_edit_mode" is always false for this function, as there is always only a "Home" team
in Edit Mode.)
Event name: "set_match_time"
Handler function: f(ctx, minutes)
Return value expected: nil or integer
This event occurs, when the game sets the mach duration. If your handler function returns an integer, then this value will be used as the match time in minutes. This way you can accelerate or slow down the matches beyound the allowed 5-30 minute range. See matchtime.lua - for an example of such script.
Event name: "set_stadium_choice"
Handler function: f(ctx, stadium_id)
Return value expected: nil or stadium_id
This event fires, when the game prepares to display the stadium image or when it is entering pre-game menus of non-exhibition modes. In addition to the actual id of the stadium chosen, the "stadium_id" parameter may have the following special values:
You handler function can change it, if it returns an integer value instead of nil. This integer value can either be a stadium id, or one of the following special values, mentioned above.
NOTE: the final stadium selection isn't actually made, until after the "set_stadium" event. So, if you want to change the stadium, or see what was eventually chosen as random/home stadium, then you will need to also register for the "set_stadium" event.
Event name: "set_stadium"
Handler function: f(ctx, options)
Return value expected: nil or number or table
This event fires, when the stadium settings are chosen for the upcoming match. The "options" parameter is a Lua table which contains the following keys: "stadium", "timeofday", "weather", "weather_effects", "season". Each of these has an integer value, as the game uses:
You handler function can either return nil, which means that other modules can receive the event and process it. Or, the handler can return an stadium id - an integer - to switch the stadium to another one. Be careful though: sider doesn't check for correctness of values, so if you switch to a non-existent stadium, the game will probably crash or go into infinite "loading" loop. For an example usage - see stadswitch.lua module. ( For backwards compatibility, returning a table like this: { stadium = stadium-id } is also supported. However, any other keys in that table will be ignored. )
To change weather, timeofday and season - use a different event, called: "set_conditions", which is documented next.
Event name: "set_conditions"
Handler function: f(ctx, options)
Return value expected: nil or table
This event fires, when the stadium settings are chosen for the upcoming match. The "options" parameter is a Lua table which contains the following keys: "stadium", "timeofday", "weather", "weather_effects", "season". Each of these has an integer value, as the game uses:
You handler function can either return nil, which means that other modules can receive the event and process it. Or, the handler can return a table of options, which are either modified or not. Returning a table of options stops further propagation of the event. You cannot change the stadium id - for that use "set_stadium" event. But you can change any of the other three settings: just assign them different values. For an example usage - see stadswitch.lua module.
Event name: "after_set_conditions"
Handler function: f(ctx)
Return value expected: nil
This event fires after "set_conditions". It doesn't allow the handler to change anything, but it does provide the context object so that the modules can react in whatever way they want.
Event name: "get_ball_name"
Handler function: f(ctx, ballname)
Return value expected: nil or string
This event fires, when the game prepares to display the ball name. Your handler function can change it, if it returns a string instead of nil. The string needs to be in UTF-8 encoding to correctly render non-ASCII characters.
Event name: "get_stadium_name"
Handler function: f(ctx, stadium_name, stadium_id)
Return value expected: nil or string
This event fires, when the game prepares to display the stadium name. You handler function can change it, if it returns a string instead of nil. (The "stadium_id" parameter is provided to handler function only as additional information - for which stadium the name is being read/modified) The string needs to be in UTF-8 encoding to correctly render non-ASCII characters.
Event name: "trophy_rewrite"
Handler function: f(ctx, tournament_id)
Return value expected: nil or number
This event fires before the game checks if trophy scenes need to be shown before (and after) the match. This is a specialized event, and is probably not very useful for modules other than "trophy.lua". The "trophy.lua" uses to enforce trophy scenes from specific tournaments. This makes it possible to have trophy celebrations for tournaments that do not have them in the original game content. (See trophy.lua, if you are really interested in how this works)
Event name: "show"
Handler function: f(ctx)
Return value expected: nil
This event fires right before the module takes control of the overlay, because the overlay was toggled on, or control switched from another module.
Event name: "hide"
Handler function: f(ctx)
Return value expected: nil
This event fires just before another module takes control of the overlay, or the overlay gets toggled off.
Event name: "overlay_on"
Handler function: f(ctx)
Return values (3) expected: string, string, table
All return values can be nil, and also the handler may return three or two, or one, or not return anything at all.
When the overlay is on, and the current Lua module is in control of the overlay, this event fires once for each frame that is displayed by the game engine (So, normally - 60 times per second). The returned string is what will be displayed by the overlay. The logic that generates this string should not be too heavy: too much processing may affect the frame rate. See examples in modules: overdemo.lua and etrace.lua
The text and image are displayed side by side: text - on the left, image - on the right. The layout options table allows to specify some formatting for the image. The following ones are supported:
Final dimensions of the image on screen are calculated using two of the three options: "image_width", "image_height", "image_aspect_ratio" - in this order of priority. If only 1 (or none of the three) are specified, then a default width of 0.1 of total screen width is assumed, and the original aspect ratio of the image is used to calculate the height.
Transparency of overlay images can be controlled by overlay.image-alpha-max settings in sider.ini. This setting affects all modules and cannot be changed at runtime. See readme.html for details on how this settings works.
Event name: "key_down"
Handler function: f(ctx, vkey)
Return value expected: nil
When the overlay is on, and the current Lua module is in control of the overlay, this event fires when user presses down any key on the keyboard. The so-called "virtual key code" will be passed as the value of "vkey" parameter. Your function can that take appropriate action, if it wants to react to such key events. A combination of "overlay_on" and "key_down" events can be used to build simple UIs in the overlay itself. See example of such UI in camera.lua
Event name: "key_up"
Handler function: f(ctx, vkey)
Return value expected: nil
When the overlay is on, and the current Lua module is in control of the overlay, this event fires when user releases a key on the keyboard. The so-called "virtual key code" will be passed as the value of "vkey" parameter. Your function can that take appropriate action, if it wants to react to such key events. (Typically, you would want to handle this event, if your module needs to avoid the standard "autorepeat" mechanism, where the operating system repeats "key_down" events when the key is held down. For "key_up" there is no autorepeat.)
Event name: "gamepad_input"
Handler function: f(ctx, inputs)
Return value expected: nil
When the overlay is on, and the current Lua module is in control of the overlay, this event fires when user presses or releases a button, or moves a stick or d-pad of the game controller. (If you have multiple controllers attached, only one will generate these input events). The "inputs" parameter is always a table containing at least one, but possibly more than one mapping of: input-name --> input-value. The "input-name" is a symbolic name that identifies the source of input: a button, stick, d-pad. See the following table for all possible combinations:
input | input-name | input-values |
---|---|---|
Button 0 | A | 0,1 |
Button 1 | B | 0,1 |
Button 2 | X | 0,1 |
Button 3 | Y | 0,1 |
Button 4 | LB | 0,1 |
Button 5 | RB | 0,1 |
Button 6 | START | 0,1 |
Button 7 | BACK | 0,1 |
Button 8 | LT | 0,1 |
Button 9 | RT | 0,1 |
Button 10 (LS push) | LS | 0,1 |
Button 11 (RS push) | RS | 0,1 |
LS (left/right) | LSx | -1,0,1 |
LS (up/down) | LSy | -1,0,1 |
RS (left/right) | RSx | -1,0,1 |
RS (up/down) | RSy | -1,0,1 |
D-pad | DPAD | 0,1,9,8,10,2,6,4,5 |
Best way to verify what names correspond to what buttons on your controller, is to try the inputdemo.lua from the modules folder. It uses overlay to display the last 20 input events - both from keyboard and from gamepad.
If your module is registered for "key_down" event, but not for the "gamepad_input" event, then sider will automatically map gamepad events into keyboard events, and emit those, for such gamepad events that are defined in the "global input mapping" configuration - in gamepad.ini.
If you do not want this automatic mapping, make sure to register for the "gamepad_input" event in your module, and then do whatever is needed with the inputs. (You may choose to completely ignore them too, if you want. See example of that in camera.lua)
IMPORTANT NOTE: Some events can fire multiple times for the same "action". That is normal, it's just how the game works internally. Make sure your module logic can handle such situations correctly.
TopSider provides a function called log. This can be used to print out any information you want into the sider.log file. You can use string.format to format your message in a way similar to what you would do with C printf:
log(string.format("My value is: %0.5f", math.pi))In sider.log it will appear with a module name prefix, like as:
[modulename.lua] My value is: 3.14159Top
Each module runs in its own environment. For detailed explanation on what an environment is - read about Lua environments in the Lua manual online, or in Programming in Lua book. What is important here is that a module has access to a limited set of globals:
standard Lua provides:
assert, ipairs, pairs, tostring, tonumber, table,
string, math, unpack, type, error, io, os, _VERSION, _G
sider adds:
log, memory, zlib, audio, match, input, _FILE
You can also enable ffi and bit modules, which are LuaJIT extensions. By default, they are disabled. To enable, modify your sider.ini like this:
luajit.ext.enabled = 1
By the way, your module can "investigate" and find out what exactly is available for it to use - this is not hard, and is left as an exercise for the reader ;-) Or... you can cheat, and look at env.lua module.
TopThe "input" library provides utility methods for managing input from keyboard and gamepads to the game. It is sometimes useful to restrict (block) such input when sider overlay is on, so that sider keyboard or gamepad controls do not also result in game actions. For example, you have logic in your overlay that uses arrow and "Enter" keys - to choose some kind of action, but the game also responds to "Enter" key as the action to activate particular mode.
input.set_blocked(block_flag, hard_block)
This function set the state of input blocking. First parameter
is a boolean indicating whether the input should be blocked or unblocked. The second
parameter (optional) specifies whether a "hard block" should be done.
Return value: nil
When the "hard block" mode is activated, the overlay toggle and module switching are also disabled. This is only rarely needed, for example when entering text from keyboard, so make sure to use carefully, otherwise you risk loosing control of your game)
input.is_blocked()
This function returns the current state of input-blocking. Returns 2 values:
The "memory" library provides a set of low-level functions that may prove useful if you're doing some advanced modding.
For example, you need some game state info that is not available in sider's context object and isn't communicated through events either. Or you want to modify some bytes in memory, because you feel really adventurous.
IMPORTANT WARNING:
USE THIS LIBRARY WITH CARE AND CAUTION,
AND IF AND ONLY IF YOU KNOW WHAT YOU'RE DOING. REALLY.
THESE ARE POWERFUL TOOLS, BUT THERE ARE ALSO DANGEROUS, BECAUSE
WRITING INTO A WRONG PLACE IN MEMORY CAN HAVE DISASTROUS CONSEQUENCES.
ALWAYS TRY TO HAVE A BACKUP COPY OF YOUR EDIT DATA AND SAVEGAME FILES.
memory.read(addr, n)
This function reads (n) bytes at memory address (addr). Return value: string of n bytes at given memory address
memory.write(addr, str)
This function writes the string of bytes (str) at the address (addr). Return value: nil
memory.guard(addr, len, fmt, default_value)
This function first checks if the address "addr" contains the value that matches the default_value given. (It automatically unpacks the byte sequence of length "len", using the format "fmt".) If the value checked does not match the "default_value", an error is raised.
Otherwise, the function returns a new object (table), which has the "read" and "write" methods that can be used to read and modify the value at given address.
memory.search(str, start_addr, end_addr)
This function searches for the string of bytes (str), in the range of memory addresses between start_addr and end_addr. Return value: address, at which the string of bytes was found or nil, if the string was not found.
memory.safe_search(str, start_addr, end_addr)
This function also searches for the string of bytes (str), in the range of memory addresses between start_addr and end_addr. However, unlike memory.search, which expects the entire range to be valid, this function checks if the memory pages it is about to access are allocated and available to the game process. Because of that, it is slower than memory.search, but it is very safe, as it skips past unavailable areas of RAM and guard pages and continues on.
Return value: address, at which the string of bytes was found or nil, if the string was not found.memory.pack(format, number)
This function converts a Lua number into one of the supported binary formats (little endian). The "format" parameter is a string that should have one of the following values:
memory.unpack(format, str)
This function converts a string of bytes (str) into a Lua number, using the format parameter to interpret the binary spec. The same values are supported for "format" param as in memory.pack function. Return value: a Lua number, converted from binary representation
These last two functions (memory.pack/memory.unpack) are useful, when used together with memory.read and memory.write, when you need to read and modify values in RAM and you know what binary formats are used for those values. See modules/memtest.lua - as the example module that demonstrates the usage.
memory.hex(value)
Utility function to output value in hexadecimal format. Depending on the type of value, the output differs slightly:
local s = 'Football' log(memory.hex(s)) --> prints "466f6f7462616c6c" in the log
local n = 12345 log(memory.hex(n)) --> prints "0x3039" in the log
memory.get_process_info()
This function queries the game process (PES2021.exe) for information
about where it is loaded in memory.
Return value: a table containing at least two keys:
memory.search_process(s)
This function searches for a string s in memory, but unlike memory.search
function, it does not take start and finish addresses. Instead, it searches
the game process sections, one at a time, until it finds the string, or
until all sections are examined.
Returns 2 values:
The "fs" library provides utility functions that are unavailable in standard Lua, but are useful when dealing with files and directories.
fs.find_files(pattern)
Searches for files that match the given wildcard pattern.
Return value: An iterator object that yields a pair of strings on each iteration: a filename and a file type,
which can be either file or dir.
For example, to list all entries - both files and directories in a given dir:
for name, ftype in fs.find_files(ctx.sider_dir .. "modules") do if name ~= "." and name ~= ".." then log("Found: " .. name .. "(" .. ftype .. ")") end end
To recursivelly walk a directory structure, with all subdirectories:
-- fsdemo.lua -- Demonstrates usage of fs.find_files function local m = {} local function walk(path) for name, ftype in fs.find_files(path .. "\\*") do if name ~= "." and name ~= ".." then log(string.format("Found: %s\\%s (%s)", path, name, ftype)) if ftype == "dir" then walk(path .. "\\" .. name) end end end end function m.init(ctx) -- list every file and every dir in BallServer content folder walk(ctx.sider_dir .. "content\\ball-server") end return mTop
This library contains a small set of utility functons to work with compressed data. An example usage could be a combination of using a "livecpk_data_ready" event to get the data, as it is read by the game, and then unpacking it, if it is compressed ("zlibbed").
zlib.compress(data)
Takes the string of data and compresses it.
Returns 2 values:
zlib.uncompress(compressed_data, uncompressed_size)
Decompresses the give string of data. The second parameter is
optional: if you know the size of uncompressed data, then it is more
efficient to provide it. If not, then uncompress will try a big
enough buffer (3-times the size of compressed data)
Returns 2 values:
zlib.pack(data)
Creates a data structure that consists of "WESYS" header, used by Konami in PES games, followed by raw compressed data. The size of the WESYS header is 16 bytes, and its format is as follows:
zlib.unpack(data)
Checks if the input is in Konami format of WESYS header, followed
by the compressed data. If so, it will try to uncompress the data,
using the size information from the header. If the data is not in
the expected format, then it is returned as is - unmodified.
Returns 2 values:
This library provides support for playing music and sound effects, which exist completely outside of the game files. Sounds are not loaded to replace existing ones, but instead you can load audio of any length and play it at any time, at any volume, using all the information and events that Sider scripting engine provides. Audio formats supported are: WAVE, MP3 and FLAC. Simple effects, such as fade-in and fade-out are supported, as well as a way to change volume and pause/resume a playing sound. See examples further down on how to accomplish these things, and also consult audiodemo.lua example module.
obj = audio.new(filename)
This function creates a new sound object and returns it as a value of Lua type "userdata". The filename parameter must contain the full pathname to the sound file. All subsequent operations are done on this sound object. Let's see some of them.
obj:play()
Start playing the sound. Notice that we use of the ":" symbol, which is a usual way that methods on objects are called in Lua. The "play" method will return immediately, regardless of how long the duration of the sound is. It will continue playing in the background, until it finishes, or is stopped, paused, faded-out at a later time.
Here is the simplest example of how to play a sound effect:
local s = audio.new("c:\\my-sounds\\ping.wav") s:play()
IMPORTANT NOTE: Due to implementation details of the audio library, you cannot create sound objects in the module's "init" function. If you try that, the "init" function will just hang and never return, blocking the game from even starting. Creating sound objects in all other event handler functions is totally fine.
Sound object lifetime
If you play a short sound effect and do not need to do anything with it afterwards, then using a local
variable - like in the code above - is fine. However, if you want to affect the play later on, for example: change
its volume, or pause it, fade out/fade back in, then you need to store the sound object in a non-local variable. You can do
that using a module-level variable, or store sound object a non-local table. We will see an example of doing that
in the next code snippet below.
obj:set_volume(volume)
Sets the volume for the sound. The "volume" parameter is a float in the ranging from 0.0 (no sound) to 1.0 (full volume). If you call this on an object that is already playing, the volume will change immediately.
Example module: audio1.lua
local m = {} -- place where we keep our sound files local content_root = ".\\content\\audio-demo\\" -- module-level variable to keep reference to our sound object local s function m.set_stadium_choice(ctx, stadium_choice) s = audio.new(content_root .. "sample.wav") s:set_volume(0.8) s:play() end function m.init(ctx) -- make content_root and absolute path, -- now that sider_dir is available in the context content_root = ctx.sider_dir .. content_root -- register event handlers ctx.register("set_stadium_choice", m.set_stadium_choice) end return m
obj:get_volume()
Returns the current volume of the sound object, as a float in the range of [0.0, 1.0]
obj:get_filename()
Returns the full filename of the sound file that was given when the sound object was created
obj:pause()
Pauses a playing sound. You can resume it again by calling obj:play() again. If the fade-out effect has been set (with a call to fade_to method before pause), then the sound will be paused after the volume fading finishes.
obj:finish()
Stops a playing sound. There is no way to resume it. If you want to play the same sound again, then you need to create a new sound object with audio.new(filename). If the fade-out effect has been set (with a call to fade_to method before finish), then the sound will be stopped after the volume fading finishes.
obj:fade_to(volume, in_seconds)
Fade the volume from the current level to the level specified by "volume" parameter, over the time period specified by "in_seconds" parameter. Note that fading starts immediately only if the sound is already playing. If the sound hasn't been started yet or is currently paused, then the fading will begin after play method is called.
obj:when_done(func)
This method allows to run some code - a function specified by "func" parameter - immediately after the sound finishes playing, either because of playing out in its entirety or due to being stopped by a call to finish method. This allows for things like playing a sequence of multiple sounds, following one another, or just in general running any logic that needs to be done after a sound finishes playing.
Now let's see some examples of fade-in/fade-out effects. We will create a module that plays one sound, when user presses "[5]" button, then cross-fades another sound, when user presses "[6]" button.
Example: audio2.lua
local m = {} local sound local sound2 function m.key_up(ctx, vkey) if vkey == 0x35 then sound = audio.new(ctx.sider_dir .. "content\\audio-demo\\uefa.mp3") sound:play() elseif vkey == 0x36 then -- do a cross-fade from one sound to another sound:fade_to(0, 3.5) -- fade-out in 3.5 seconds sound:finish() sound2 = audio.new(ctx.sider_dir .. "content\\audio-demo\\sample.wav") sound2:set_volume(0) -- start from 0 volume sound2:fade_to(1, 2.5) -- fade in in 2.5 seconds sound2:play() end end function m.overlay_on(ctx) return "press [5] - to play music, [6] - to crossfade to another sound" end function m.init(ctx) ctx.register("key_up", m.key_up) ctx.register("overlay_on", m.overlay_on) end return m
Another, a little longer example: let's start some music, when user presses ["5"] button. During a match, if we press Start button to pause the game, let's have the music fade out. Then when the user resumes the game, fade the music back in.
Example: audio3.lua
local m = {} local sound local volume local function pause_sound() if sound then -- remember volume of this sound, so that we can resume playback later at the same volume volume = sound:get_volume() -- pause with 2.5 second fade-out sound:fade_to(0, 2.5) sound:pause() end end local function resume_sound() if sound then -- resume play with 2.5 second fade-in sound:fade_to(volume, 2.5) sound:play() end end function m.data_ready(ctx, filename) if filename == "common\\script\\flow\\Match\\MatchPrePause.json" then log("game loaded: " .. filename) -- pause sound when Pause Menu shows up pause_sound() elseif filename == "common\\script\\flow\\Match\\MatchPostPause.json" then log("game loaded: " .. filename) -- resume sound, when Pause Menu exits resume_sound() end end function m.key_up(ctx, vkey) if vkey == 0x35 then if sound then -- already playing. Let's stop it sound:finish() else -- not playing: Let's start a new sound sound = audio.new(ctx.sider_dir .. "content\\audio-demo\\uefa.mp3") sound:set_volume(0.7) sound:when_done(function(ctx) -- we are finished playing: set sound non-local var back to nil sound = nil end) sound:play() end end end function m.overlay_on(ctx) return "press [5] to play music" end function m.init(ctx) ctx.register("livecpk_data_ready", m.data_ready) ctx.register("key_up", m.key_up) ctx.register("overlay_on", m.overlay_on) end return m
Let's see an example of 3-sound sequence:
Example: audio4.lua:
local m = {} -- we will keep references to sound objects in this table local sounds = {} function m.key_up(ctx, vkey) if vkey == 0x35 then local s1 = audio.new(ctx.sider_dir .. "content\\audio-demo\\sample.wav") s1:when_done(function(ctx) sounds[s1] = nil local s2 = audio.new(ctx.sider_dir .. "content\\audio-demo\\uefa.mp3") s2:when_done(function(ctx) sounds[s2] = nil local s3 = audio.new(ctx.sider_dir .. "content\\audio-demo\\toggle.wav") s3:when_done(function(ctx) sounds[s3] = nil log("sequence done playing") end) sounds[s3] = true s3:play() end) sounds[s2] = true s2:play() end) sounds[s1] = true s1:play() log("sequence started playing") end end function m.overlay_on(ctx) return "press [5] to play a sequence of sounds" end function m.init(ctx) ctx.register("key_up", m.key_up) ctx.register("overlay_on", m.overlay_on) end return m
One last example is a sort of a fun one. It plays Cristiano Ronaldo's "suuu!" shout, when he scores a goal AND does his special "windmill" celebration with high jump and full turn around. The sound effect is triggered, when the game loads a specific animation file. The MP3 file has 6 seconds of silence, followed by the shout. The delay achieves two things: 1) we time the sound with the pose that Ronaldo strikes; 2) if the game decides to load a different celebration, we cancel the playback of the sound.
This may all feel a little too complex, but it just shows the flexibility of the audio library, and kind of logic you can put together - sensitive to specific sitations.
Example: cr7_audio.lua:
-- cr7_audio.lua -- ================ -- Ronaldo unique celebration sound for PES 2018 local m = {} local cr_sound function m.data_ready(ctx, filename) --log(filename) if string.match(filename, "dml_goal_Star_jump01.gani") then if not cr_sound then log("Ronaldo celebration loaded: " .. filename) -- Play C.Ronaldo "suuu!" sound, when he scores and does his unique celebration -- cr_suuu_d6.mp3 has 6 seconds of silence, followed by "suuu!" cr_sound = audio.new(ctx.sider_dir .. "content\\audio-demo\\cr_suuu_d5.mp3") cr_sound:set_volume(1) cr_sound:play() cr_sound:when_done(function() cr_sound = nil end) end end end function m.init(ctx) ctx.register("livecpk_data_ready", m.data_ready) end return m
That's all for now. Enjoy programming and game modding with Sider! :-)
Back to top