Sider 7 Lua Module Programmers Guide

For sider.dll version: 7.3

  1. Introduction
  2. Module structure
  3. Context object
  4. Supported events
  5. Logging
  6. Module environment
  7. Input library
  8. Memory library
  9. File system library (fs)
  10. Zlib library
  11. Audio library
  12. Match library
  13. Custom events

Top

1. Introduction

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.

Top

2. Module structure

If 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:

  1. Your need to create a new table
  2. Provide a function, called "init" in that table, where you need to do any initialization logic for the module and register for the events your module is interested in.
  3. Return that table as the last statement
Example module: test.lua
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).

Top

3. Context object

Context 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 7.0.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:
  • league: number of league matches already played
  • cup/play-off:
    • 46 - first round of play-off,
    • 47 - second round of play-off,
    • 51 - quaterfinal,
    • 52 - semifinal,
    • 53 - final
  • You can use this together with ctx.tournament_id to identify a final match of a particular 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:
  • 65533 - home stadium
  • 65534 - random stadium
  • 65535 - not chosen yet
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:
  • 2 - enforce weather effects (rain/snow falling)
ctx.match_time Duration of the match in minutes.
ctx.difficulty Difficulty that was chosen: 0 - Beginner, 1 - Amateur, ... 5 - All-star, 6 - Legend
ctx.extra_time 0/1 flag to indicate if extra time will be played
ctx.penalties 0/1 flag to indicate if penalty shootout will be played
ctx.substitutions number of substitutions allowed in normal time
ctx.substitutions_in_extra_time number of substitutions allowed in extra time
ctx.custom_evt_rbx Address of "sider_custom_event_rbx_hk" function. Needed for implementing custom event generators. For more details, see Custom Events Guide: custom-events.html
ctx.kits table of functions for kit manipulation. (See more details further down)

The context object also contains the following functions:

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.

ctx.get_event_id(event_name)

Returns the unique event id (number) for a given event name. For more details, see the "Custom Events" guide here: custom-events.html

3.1. ctx.kits

It is a table of helper functions to manipulate kits (uniforms).

IMPORTANT NOTE:
kit operations only work for "licensed" teams, in other words, for those that have real kits, not ones made in game editor. If your team is unlicensed, use Kit Studio or Team Licencer from SimpleTools collection (both tools by zlac) to convert it to a licensed team.

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:

Top

4. Supported events

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: "set_match_settings"
Handler function: f(ctx, options)
Return value expected: nil or table

This event fires, after the match settings had been chosen for the upcoming match. The "options" parameter is a Lua table which contains the following keys: "difficulty", "extra_time", "penalties", "substitutions", "substitutions_in_extra_time". 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. For an example usage - see matchset.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: "context_reset" (since 7.3.0)
Handler function: f(ctx)
Return value expected: nil

This event fires when the sider context is reset. Typically, it happens when the game goes back to the Main Menu, but also in some situations in Edit Mode, when new team is selected.

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, schedule_entry)
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.

The last parameter - schedule_entry - is new in sider 6.3.4. It is set to nil in all usual scenarios where stadium name is chosen, except for the case when a Master League screen shows a small fragment of the schedule (schedule bar) with the 4 matches: 1 that just happened, and 3 in the future. This is new in PES 2020 Master League (was not there in PES 2019). In this use-case the "get_stadium_name" event fires for each of those 4 matches, and the schedule_entry parameter is a table that contains information about the corresponding match as its keys:

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: "display_frame"
Handler function: f(ctx)
Return value expected: nil

This will fire every time the game renders a frame. Be careful with how much logic you put in this function: if there is too much complex processing, you will basically kill your frame rate.

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:

inputinput-nameinput-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.

Top

5. Logging

Sider 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.14159
Top

6. Module environment

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, fs, 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.

Top

7. Input library

The "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:

  1. true - if input is currently blocked, false - otherwise
  2. true - if "hard block" is also on, false - otherwise

Top

8. Memory library

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:

Return value: string of bytes, representing the number in the format specified by the "format" parameter

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:

  1. address of string s in memory, if found, or nil - otherwise
  2. table with section info, in which the string was found.

memory.allocate_codecave(num_bytes)

This function allocates a new memory block of num_bytes bytes, and sets its "protection" in such a way that if machine code instructions are written into it, they will be allowed to execute. This is what is called a "code cave": a place in memory where you can write new machine code. (This is useful mostly for advanced modding techniques, where you may need to write new code to the process memory and then redirect execution flow to it.)

Return value: an address of new memory block.
(If unable to allocate memory, the function will throw an error with details)

Top

9. File system library (fs)

The "fs" library provides utility functions that are unavailable in standard Lua, but are useful when dealing with files and directories.

fs.make_dirs(path)

Creates a directory and all intermediate directories in the path, if necessary.
Return value: nil or a string containing error message, if there was an error.

NOTE: If directory already exists, the return value will contain "Directory already exists" string. Your logic can do whatever it wants with that information.

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 m

Top

10. Zlib library

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:

  1. string with compressed data, or nil - if an error occured
  2. nil, or string with error message, if something went wrong

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:

  1. string with uncompressed data, or nil - if an error occured
  2. nil, or string with error message, if something went wrong

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:

Returns 2 values:
  1. string containing the data structure, or nil - if an error occured
  2. nil, or string with error message, if something went wrong

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:

  1. string containing uncompressed bytes, or original data, if the input is not in WESYS-format, or nil, if some error occured
  2. nil, or string with error message, if an error occured.

Top

11. Audio library

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:get_state()

Returns the string description of the current state of the sound object. Possible values are:

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

Example of using "get_state" method, with overlay showing the information on the currently playing sound: its filename, volume, and state

Example: audio5.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.key_up(ctx, vkey)
    if vkey == 0x35 then
        if sound == nil or sound:get_state() == "finished" then
            -- 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:play()
        elseif sound:get_state() == "playing" then
            pause_sound()
        elseif sound:get_state() == "paused" then
            resume_sound()
        end
    end
end

function m.overlay_on(ctx)
    if sound ~= nil then
        return string.format("sound %s (volume: %0.2f): %s", sound:get_filename(), sound:get_volume(), sound:get_state())
    else
        return "no sound. Press [5] - to play/pause"
    end
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:

local m = {}

local cr_sound

function m.data_ready(ctx, filename)
    if string.match(filename, "goal\\cut_data\\goal_celebrate_0279_append02.fdc") 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_d6.mp3")
        cr_sound:set_volume(0.4)
        cr_sound:play()
        cr_sound:when_done(function()
            cr_sound = nil
        end)
    end
    if cr_sound then
        local another_celeb = string.match(filename, "goal\\cut_data\\goal_celebrate_(%d+)")
        if another_celeb and tonumber(another_celeb) ~= 279 then
            log("Another celebration loaded: " .. filename)
            -- cancel SUUU sound
            cr_sound:finish()
        end
    end
end

function m.init(ctx)
    ctx.register("livecpk_data_ready", m.data_ready)
end

return m

Top

12. Match library

This is a library that allows you query for some statistics about the current ongoing match. As of 7.0.0 release, it is still in experimental stage, so use with caution. It is actually disabled by default, and in order to enable this library, you need to have this line in your sider.ini:

match-stats.enabled = 1

match.stats()

This function returns a table, containing information about live match. If there are no stats available right now (because, for example, there is no match going on), then the return value will be nil

If a table is returned, it can contain the following keys:

  1. home_score, away_score: current score
  2. pk_home_score, pk_away_score: current score in penalty shootout (if one is happening)
  3. period: period of the game: 0 - unknown, 1 - first half, 2 - second half, 3 - 1st exra time, 4 - 2nd extra time, 5 - penalties
  4. clock_minutes, clock_seconds: current match clock
  5. added_minutes: nil or number of minutes added at the end of a half (announced injury time)

See modules/mstats.lua - for an example of using this function.

Top

13. Custom events

New in sider 7.3.0 is the support for Lua modules to both generate and handle custom events. These are events that are NOT generated by sider. Instead, it is the responsibility of a Lua module to do so. It is an advanced topic covering multiple modding techniques needed to make everything work together.

Please see the detailed guide here: custom-events.html if you are interested to learn about it.


That's all for now. Enjoy programming and game modding with Sider! :-)
Back to top