King Arthur's Gold

King Arthur's Gold

Not enough ratings
Modding for everyone - english
By salty Snek and 1 collaborators
For a quick start, begin from 4th section.
This guide will help you with:

  • Getting rid of feeling lost
  • Coding basics you need to know
  • Installing and setting up scripting, spriting and audio tools
  • Understanding KAG modding system and distinctive features
  • Loading mods
  • Creating your first mod
  • Using additional tools like Aseprite and Audacity for spriting and producing audio assets
  • Reviewing advanced modding aspects

    Share your feedback here or ask a question at KAG Modding Discord[discord.gg]
2
   
Award
Favorite
Favorited
Unfavorite
Before we begin...
Let me introduce myself.

I am NoahTheLegend[github.com] also known as salty Snek.
A KAG[github.com] player for over 6 years and KAG modder for over 3 years.

I started modding with Vamist's and Cocok's help, and I really appreciate they spent their time and were patient to dumb me.
My first mod was Cats & Mice, a rework from classic KAG, with assets from Punk123's version.
It didnt go well like many other things I worked on for the first time, but I had fun exploring and learning coding, especially when I had immediate results.

Later I started helping Xeno, Skemonde and Frank with "Territory Control - A New Hope". It lasted for a while and looking back at original TC, the "Hope TC" version gained it's own and quite big, independent community.
Whilst working on small changes in the "Hope TC" I accidentally spent a year creating and updating many other mods.

Later, Blav (aka Blavier) revived an old mod called King Arthur's Wooden Warfare - KAWW (now Armored Warfare), and I offered my help.
Suddenly and accidentally, now this is the second biggest mod with ~10k unique visitors, which also periodically takes the role of "the most popular server", taking the spot of CTF when online.

I wont say everyone could create a big and popular mod cause it requires the understanding of game design and what people want to play. Many of mods die after a week or two after release and almost never appear again.
You may think then, modding is a thankless job. And you would be correct - in a game with so few active players.

But nevertheless, you could be doing that just for yourself and your friends!
Chess, Super Mario Kart or even actual 3D Minecraft or Doom-like PvP shooter.
Anything funny you can imagine - enjoy your own creations :steamhappy:


Post scriptum

If you ever played KAG before this guide was posted, you might already know me well, and most likely as an unkind and toxic player.
I am.

However I have also been participating in the game's life and events: hosting multiple servers; helping some people with all kind of things; organizating own events and social communities; participating to GitHub repo and other KAG-related projects.

This is what makes me happy.
Promoting the game and discussions around it keeps KAG alive. That is important considering the latest activity drops, both in-game and in social medias like Steam forum.
Getting started
Coding basics

This part, actually, is better to keep for your "homework." There is no better explanation than understanding how things work on your own.

In fact, you can use almost any object-oriented programming language to learn, like C++, C# or Java - it is up to you.
Here's a list of things you need or want to know before you start modding:

NECESSARY - Variables - Data Types - Operators - Functions - Conditionals (if-else, switch-case) - Loops - Arrays - Scopes and Imperative Programming

PREFERABLE - Classes and Objects - Events (hook functions) - Comments and why they are necessary - Arguments and Parameters, Overloading functions - What is Hard-code and Brute Force in programming

UNNECESSARY (but still preferable) - More Classes and Objects researching - Software design patterns - Pointers, References, Handles and difference between them - Namespaces and Enum - SOLID principles


Theory

KAG is originally written in C++, but that part is closed to us modders. We only have the scripting language AngelScript[www.angelcode.com].

Scripting is the core of modding (unfortunately). It's the most routine and visually deprived part - essentially a playground for your brain and imagination. When you start to code, especially as a beginner, you’re left to figure things out in the dark, solving an ancient puzzle. You don’t know what to do or where to start. Touching thing A breaks thing B, and fixing thing B breaks thing A.
Following the next tips should help.

The first step is defining the goal you want.
That doesn't require understanding of specific methodologies or knowledge. For example, let’s say you want to create a dash for knights (like in Celeste).

This task should be split into smaller steps, where A is the first action and B is the goal. Everything in between fully relies on you. Use your logic to design the subjective route between them.
Which script should I modify? How? Where? Why is my console painted red?.. That's all a part of the process.

Here's how I would design it and fix errors, for example (do not repeat that order right now. read it exceptionally for example):



1. Find and open any script related to knights (let's say we know it’s KnightLogic.as).

2. Define the event for the dash - key pressed? Let it be right-click (key_action2).

3. I know I need to look for matching words in the vanilla code. And I found key_action2 in some methods
this.isKeyPressed(key_action2)
and
this.isKeyJustPressed(key_action2)
I should try the first. Wait, why is it in the onTick() function, while there’s also onRender()? Well, I don’t want to overload myself with info, so I’ll try the same in onTick() in my code!

4. I pasted isKeyPressed(key_action2) and a temporary print() messaging "do something?" into my code and got this error in the console:


5. The yellow frame shows the path of the script and line 215. In the red frame, the error says what’s wrong - I forgot a semicolon somewhere. Adding it and recompiling - still an error?!


6. The “no matching symbol” error, shown in the red frame, means nothing is found to call or interact with, isKeyPressed(const keys) shows the subject and its parameter.
How was it in the vanilla code? I need to run it like this.isKeyPressed(key_action2)! Besides, it’s in an if-else condition for a reason. So now my code looks like:

if (this.isKeyPressed(key_action2)) { print("do something?"); }

I clicked once and see do something? in my console. But why are there 10 messages? Hm, I remember seeing the isKeyJustPressed method. Thats the needed one.

7. Now I need to make the knight move. Searching matches for the word "move" doesn’t give clear results. Maybe synonyms? Speed? Dash? Velocity - that’s it. Luckily, I found this:
f32 aim_angle = (this.getAimPos() - this.getPosition()).Angle(); this.setVelocity(Vec2f(8, 0).RotateBy(-aim_angle))




And it works. Technically, at least. We still have the spam-click issue, but we’ve achieved what we planned - point B. At this moment we can start moving to point C.


Practices used
  • As you can see, I splitted the whole task into smaller steps and focused on them instead of final goal. It's really important to keep tracking only what you need at the moment and shift your focus outward when you get stuck or don't know how to fix an error.

  • In the example, I tried to simulate a condition, where I, as a coding noob, still know something about functions and tools I used. That is a mandatory you need to know - the minimal BASE of programming.
    The example itself only visualizes the technique for avoiding feeling lost.

  • Other techniques like "search matches by word" and key-combos, e.g. CTRL+F are very helpful. Yea, you can code with Notepad in Win7. But for your effectiveness and time management, you want to install the programs that fit your job the best.
Modding tools
KAG Tools[github.com]
A time-saving launcher that includes a server browser and a manual (essentially a declaration of accessible functions, methods, namespaces, and more).




Scripting

For scripting, as mentioned in the previous section, you can technically use Notepad to write code. However, doing so unnecessarily limits your capabilities and wastes time on repetitive tasks. Instead, consider installing specialized tools that significantly enhance productivity with just a one-time setup.

Visual Studio Code[code.visualstudio.com] - I have been using it for past 3 years. This software is more than just a text editor - it offers thousands of plugins, envrionment simulation, Git integration, Web servers, graphic tools and AI assistance. It also supports writing snippets (kind of macros) and advanced search using regular expressions.


The downside is its relatively slow startup due to plugin initialization and the overall bulkiness of the software.

Sublime Text[www.sublimetext.com] is another powerful option, likely the second most popular in its category. It offers some features similar to Visual Studio Code but has fewer plugins available.

Notepad++[notepad-plus-plus.org] is a minimal tool for coding in every sense. It's lighter and faster than most editors - only Notepad itself is more lightweight.



Spriting and Mapping

Maps are loaded from .png files, so most mapping needs are covered with a single graphic editor.

Aseprite[www.aseprite.org] is versatile and easy to use. Most spriters I know, including myself, have been using it for years. Despite being a small program, it offers a wide range of tools and settings to enhance your work. It also supports plugins.
The downsides include reduced resolution for large images (optimized for zoomed-out views) and the fact that it is paid (though a free trial is available). Look for a "community" version on the internet.

Krita[krita.org] is less popular but entirely free. Some people I know have used it in the past.

Piskel[www.piskelapp.com] is a free online graphic editor. While I haven't personally used it, I recommend a desktop app for faster uploading and editing.

That said, you can use any graphic editor you prefer.



Sound

Audacity[www.audacityteam.org] is the only free and relatively powerful tool I know of, although it can be quite difficult to master.



Plugins

Here I can't suggest much. You definitely would want a syntax highlighter. There is a plugin "AngelScript" in VSC, however you can use anything, i guess. Watch a tutorial about installing and enabling plugins for your software on YouTube.

Loading mods
KAG requires a specific set of actions to load mods.
You can open the KAG folder using KAG Tools or navigate to it manually at:
Steam/steamapps/common/King Arthur's Gold (hereinafter - KAG/).



1. If you are starting from scratch, create a folder for your mod in KAG/Mods/. Avoid using spaces and symbols (except for the dash "-" and underscore "_").

2. If KAG Tools is installed, go to the Mods tab and enable your mod. Alternatively, locate the mods.cfg file in KAG/ and write the name of the folder you created on the third line (below the two #comments).

3. If KAG Tools is installed, set the gamemode to one of the options in the drop-down list. Alternatively, modify KAG/autoconfig.cfg by searching for the line containing sv_gamemode.
This will load the default rules from vanilla for your mod.
Now you have a workspace to begin modding.

4.1. To modify something, paste the necessary files into your mod folder and DO NOT rename them.
4.2. To add something, rename the files. If it’s an entity, item, or structure (of type CBlob), you must also edit its config file to assign it a unique $name.




Important

When KAG compiles a mod, it replaces any vanilla file with the same name if it finds one in your mod folder.
This "replacement" is temporary - your local files remain unchanged, and you can still #include vanilla files.

The inclusion of files is managed by CFileMatcher, which you can also use in your scripts.

CFileMatcher uses a "first match" method. It checks your mod folder first before searching Base/.
For example, if you have a file at the path mymod/KnightLogic.as:
#include "KnightLogic.as"; // will copy the script from mymod/ #include "Knight/KnightLogic.as"; // will copy the script from Base/Entities/Characters/Knight/

However, KAG doesn’t support hierarchies between multiple mods that include the same files. In such cases, the compiler may fail.

Creating your first mod
Practice example

In this example we will first define our goal. You should make your own, but if you're struggling, repeat the example:
Add a new drill that any class can use, with custom stats, sprite and sound, also it should insta-break dirt and wood.
Additionally, we will modify both drills so they do not drop from your hands when they overheat.



1. Follow the steps from Loading mods section
To simplify the example, i will be shortening our workspace path to "mod/" and vanilla files to Base/. Set the gamemode to Sandbox.

2. Find Drill files in Base/
Follow the path Base/Entities/Industry/Drill/.
If you have Visual Studio Code installed, in File > New window open Base folder, key-combo CTRL+P opens file searcher. Open Drill.as and right-click it's tab, select Reveal in File Explorer. Other programs may have similar features.

3. Select and copy Drill.as, Drill.cfg, Drill.png, Drill.ogg to mod/SuperDrill/

4. Copy Drill.as to mod/Drill/

5. In SuperDrill/ rename all files to SuperDrill with corresponding extension (.as, .png and so on)

6. In the text editor, open SuperDrill.cfg and SuperDrill.as
In the config, CTRL+F the word "drill". Rename the source files (Drill.png, Drill.as and so on) to SuperDrill with corresponding extension.
Unique name $name should be superdrill, or super_drill.

Now we have to edit the main script - SuperDrill.as.

7. From line 10 to 30 you may change the stats for the new drill.
Replace some u8 (unsigned char) to u16 (short unsigned int) to prevent overflowing. You can either manually identify variables that may overflow throughout the script or use a quick replacement tool.


8. Replace this.server_DetachFrom(holder) with this.set_u16(heat_prop, heat_drop-1) in both drill files.

9. There is required_class variable on the line 36 - delete it and the lines using it (210 and 506) to let anyone use the superdrill. Remove the else {} entirely after the scope of line 210
Despite i've mentioned the lines 210 and 506, you should always search for the word matches on your own. Sometimes globally - if the script is included to other scripts.

10. Find DestroyTile in the script and edit that part of code in such way
u8 damage = 1; if (map.isTileGround(tile) || map.isTileWood(tile)) { damage = 10; // insta break } map.server_DestroyTile(hi.hitpos, damage, this);
When done, open the console and write rebuild, then write /s superdrill or !superdrill to chat to spawn it.

11. In graphic editor open SuperDrill.png and edit it how you like
The frames in the second half of the sheet are used for the overheated sprite layer, which is managed in SuperDrill.as.
Restart your game to update sprites.

Keep in mind that the sprites are implicitly split in a grid set in blob's config file.
The drill sprite is 64x64 pixels, but the values set in the config are width=32, height=16. This means the drill has 8 tiles on spritesheet to utilize. Each tile has its own index - starting from left to right.

If there is no more space to the right to fit a tile - it moves to the next line, e.g. nothing changes if you set the drill spritesheet width to 64+31, or 65+16, however if you add the minimum, 32 pixels, the game will change indexing from
0 1 2 3 4 5 6 7
to
0 1 2 3 4 5 6 7 8 9 10 11
breaking the frames order in animation or affecting some other declarations.

12. In sound editor (Audacity on the screenshot), you only need equalize, reverse, reverb, pitch and tempo tools for quick editing. Watch this tutorial
1. Adjust volume level to that you have in-game
2. Use Selection mode (1st button) to select a part of the track or Volume mode (2nd button) to lower the volume in the track.
3. If you want to increase volume, apply the Volume and Compressor > Amplify effect.
4. To set a playback loop, enable it and shift the slider above the track.
Once done, export it at File > Export Audio menu, replace the file in SuperDrill/ and restart your game.


Mainly we've done everything planned. However if you want to add superdrill to a shop, or a chest loot, you should modify other files.
Let's add it to builder shop and as a rare loot for chests.

13. Copy BuilderShop.as from Base/Entities/Industry/CTFShops/BuilderShop/ and LootCommon.as from Base/Entities/Common/Loot/ to mod/
1. At line 40, you can see the list of items added to the builder shop. The items fill the shop menu in almost the same way as sprite sheet indexing, so if you want to save the item order, replace some items at the end of the list and copy the first item declaration (drill) by replacing the wanted arguments.
Also increase the shop menu size to be 1 row taller.
CTFCosts namespace is declared in Costs.as file and Descriptions at Descriptions.as, but you can ignore those and hardcode the values.
2. In LootCommon.as go through each match of the word "drill" and make a new subject for the new superdrill.





Tips and commands
  • Open the console with the Home key or F5 in the staging version of the game (hereinafter referred to as "staging").
  • The console can execute simple commands:

    CBlob@ b = server_CreateBlob("drill", 0, getPlayer(0).getBlob().getPosition());

  • After saving a modified file, type rebuild in the console to update it. This does not work with shared scopes and sprites (except for maps).
  • Some pointers require a null check. Mostly, these are functions and methods returning CBlob, CSprite, CSpriteLayer, CParticle, AttachmentPoint and some other, or arrays containing them.
  • To spawn an item, type /s blobname to chat (or !blobname for old mods)
  • If you are not in localhost (singleplayer), server-side commands like rebuild or code execution require /rcon at the beginning, like this:

    /rcon printf("sussy player " + getPlayer(0).getUsername());
Advanced - set(), get() methods
set(), get() methods for primitive types and common mistakes

Keep in mind, even though copy-pasting from Base/ is the easiest method - it's not always the best. We duplicated an unnecessarily large part of the code into another file. While this does not affect game performance, it makes future adjustments and editing more difficult for a human.

Ideally, we should use one script file and modify it in a way that assigns properties to the blob running the script. If you already understand how objects and pointers work, you should know that hooks like void onTick() are "independent" and serve only as "reassemblers" for their arguments.

For example, if there is a global variable in a script running on blobs A, B, and C, and you assign it a new value from "A" blob:

void onTick() { if (this.getName() == "A") { global_var = 5; } }

All three blobs will have their global_var set to 5.

To avoid this, most main classes in the game have "set" and "get" methods, such as:
set_string(string key, string value), add_f32(string key, float increment), and sub_s8(string key, s8 decrement).

By using just getName() or set methods, we can already refactor Drill.as to replace global values with relative ones extracted from the blobs running it.
In SuperDrill.as, we should only have void onInit() to initialize all values extracted with get() methods in Drill.as.

Technically, this is not much different, but following these Computer Science principles helps you learn more, save time and avoid unexpected errors.


set(), get() methods for object types and pointers
Setting objects, i.e. CBlob or arrays is also possible in KAG, although works in a not very intuitive way.

// example class to store the amount of hits class DrillCache { u32 hits; DrillCache() { hits = 0; } void addStat() { hits++; } u32 getStat() { return hits; } }; void onInit(CBlob@ this) { // ... other code above // initialize empty stats Vec2f[] hitpositions; CBlob@[] blobs_hit; DrillCache cache; // add the pointers to memory this.set("hitpositions", @hitpositions); this.set("blobs_hit", @blobs_hit); this.set("cache", @cache); } // update stats if they're present void addStats(CBlob@ this, Vec2f tile_pos = Vec2f_zero, CBlob@ blob = null) { // initialize a pointer to Vec2f[] array, because we are adressing to it in the memory Vec2f[]@ hitpositions; if (!this.get("hitpositions", @hitpositions)) { warn("hitpositions fail"); return;} // don't be afraid of the @[]@, it's a pointer (second @) to an array of pointers (first @[]) CBlob@[]@ blobs_hit; if (!this.get("blobs_hit", @blobs_hit)) { warn("blobs_hit fail"); return;} DrillCache@ cache; if (!this.get("cache", @cache)) { warn("cache fail"); return;} if (tile_pos != Vec2f_zero) hitpositions.push_back(tile_pos); if (blob !is null) blobs_hit.push_back(blob); cache.addStat(); // we were directly adding data to the objects in memory, hence we don't need to set the data again } void printStats(CBlob@ this)
{
Vec2f[]@ hitpositions;
if (!this.get("hitpositions", @hitpositions)) { warn("hitpositions fail"); return;}

CBlob@[]@ blobs_hit;
if (!this.get("blobs_hit", @blobs_hit)) { warn("blobs_hit fail"); return;}

DrillCache@ cache;
if (!this.get("cache", @cache)) { warn("cache fail"); return;}

print("Total hits: " + cache.getStat());
for (uint i = 0; i < hitpositions.length; i++)
{
print("Hit position: " + hitpositions[i].x + ", " + hitpositions[i].y);
}

for (uint i = 0; i < blobs_hit.length; i++)
{
if (blobs_hit[i] is null)
{
// remove the pointer to avoid bad memory access
i--;
blobs_hit.removeAt(i);
continue;
}

print("Hit blob: " + blobs_hit[i].getName());
}
}

The example for the drill code shows a step-by-step handling of object-type data.
If you want to test it on your own, in the places of destroying tile and damaging blob call the addStat() with required arguments.

Add this line to onTick() to print the data when pressing R.
if (getControls().isKeyJustPressed(KEY_KEY_R)) printStats(this);
Advanced - config files
KAG's config files
So-called .cfg files are an essential part of KAG and are used everywhere. However, any problem occurring during compile time will not show errors and will most likely crash the game.

Additionally, the formatting and syntax are outdated. You don't need to remember or worry about details like "what are & and @ before variables?" or "where should/shouldn't there be a semicolon at the end of a line?" Instead, just copy and paste existing configs with the desired properties and edit source files, animations, gibs, or shapes they load.

Some variables accept values of 0 (false) and 1 (true).

Blob config inspection
@$sprite_scripts - scripts that run CSprite@ hooks.
$sprite_gibs_start - gibs (particles spawning when the blob dies).
$sprite_animation_start - sprite animations. Sprite animation names don’t matter, but their values do.
@$shape_scripts - list of scripts that run CShape@ hooks.
f32 shape_mass - affects AddForce() in both code and the box2d physics engine. Never set this to 0.
f32 shape_radius - the hexagonal shape radius, unless manually set.
f32 shape_friction - friction with tiles and other shapes. Strongly influenced by RunnerMovement.as.
f32 shape_elasticity - onCollision() bounciness multiplayer for box2d.
f32 shape_buoyancy - buoyancy multiplier.
f32 shape_drag - affects velocity damping, or "windage".
bool shape_collides - forces the shape to collide with everything.
bool shape_ladder - makes the shape behave like a ladder for other blobs.
bool shape_platform - enables one-way collision from above.

@f32 verticesXY = # Manually assigned shape boundaries, requires 6-16 arguments (3-8 XY points). 0.0; 0.0; # Top-left, X = 0, Y = 0 8.0; 0.0; # Top-right 8.0; 8.0; # Bottom-right 0.0; 8.0; # Bottom-left

u8 block_support - Sets support for other tiles.

@$attachment_points = # List of attachment points, requires arguments: X offset; Y offset; socket/plug=0/1; (socket provides a slot, other blobs can attach to this, plug means this blob attaches to other blobs and creates a temporary attachment point for the provider-blob's list while this is attached to the provider-blob) controller=0/1; (when set to 1, redirects key inputs of attached blob to attachment point if socket and vice-versa) radius; (used in scripts, generally for the maximum range of getting inside vehicles) PICKUP; -2; 2; 1; 0; 0; PASSENGER; 0; 0; 0; 0; 0;

gamemode.cfg inspection
teams - team collection. Can be accessed using CTeam and CRules.

Gamemode properties (some are legacy and won't work)
allow_suicide = no
attackdamage_modifier = 0
can_show_hover_names = no
chat = no
coins_arrows_cost = 0
coins_bomb_cost = 0
coins_build_percentage = 0
coins_damage_enemy = 0
coins_death_drop_percentage = 0
coins_heal_cost = 0
daycycle_speed = 0
daycycle_start = 0
death_points = 0
engine_floodlayer_updates = no
friendlydamage_modifier = 0
kill_point = 0
map_fire_update_ticks = 0
map_water_layer_alpha = 0
map_water_render_style = 0
map_water_update_ticks = 0
mapresource_arrow = 0
mapresource_gold = 0
mapresource_stone = 0
mapresource_thickstone = 0
mapresource_tree = 0
minimap = no
mirrormap = no
nearspawn_multiplier = 0
no_shadowing = yes
output_history = no
party_mode = no
player_light_intensity = 0
player_light_radius = 0
playerrespawn_seconds = 0
selfkill_points = 0
showscoreboard = no
support_added_vertical = 0
support_cost_castle = 0
support_cost_wood = 0
support_factor = 0
warmup_barrier = no
Advanced - client-server communication and syncing
Definition of client-server and how it works in KAG
A connection is established between your client and the server using the UDP protocol to share data. The data transferred between the server and clients consists of a "stream" of zeros and ones, sent in groups, called packets, with a certain interval.

We call the client the game running on your local computer, while the server is the process running on another computer, responsible for hosting, processing, and synchronizing data to clients. It manages game rules, anti-cheat measures, file verification, and many other critical tasks.

However, the server does not handle or sync everything for each client, such as player movement, physics, and camera positioning. Instead, this data is processed asynchronously, with the server periodically synchronizing the most critical aspects.

Additionally, KAG uses a principle called player authority, where some data is generated on the client first, then sent to the server, and only after that, synchronized for other clients.

For example, when a player's blob moves in KAG, from the side of the player's client it does not wait for the server to verify the movement before updating the position locally. This eliminates movement lag for the player, but it means other players see the movement with a slight delay. This system can lead to issues such as position desync, e.g. passing through closed doors, the "glue" bug, damageless stomps, and exaggerated pickaxe reach.

There is no universal agreement on whether this approach is objectively better or worse than full server-side verification, but many modern games implement player movement prediction - unlike KAG and many other 2D games. With this in mind, you should design your code carefully, as data may desynchronize, or you may need to manually handle it properly.


Utilize onCommand() and SendCommand()
In KAG, manually sharing data between the client and server requires the following steps:



1. Assign a command ID (a u8, which is a strict limitation) in onInit() or anywhere else just once, using addCommandID(string key).
void onInit(CRules@ this) { // make sure this CRules script is running after all other that interact with engine, in the script list in gamemode.cfg this.addCommandID("do_client_sound"); }

2. Serialize the data into a binary format using CBitStream and its methods and send a command using SendCommand(u8 command_id, CBitStream stream)
void onBlobDie(CRules@ this, CBlob@ blob) { if (!isServer()) return; // good practice to avoid too much leveling in file, which is inversed because localhost (singleplayer) returns true both to isClient() and isServer() bool play_sound = blob !is null && blob.getPlayer() !is null; CBitStream params; params.write_bool(play_sound); this.SendCommand(this.getCommandID("do_client_sound"), params); }

3. In the onCommand() hook, deserialize the data from CBitStream in the exact order it was written.
The order of reading matters because CBitStream does not inherently track the structure of the data. You must provide explicit instructions for how to read it; otherwise, the retrieved data will be corrupted.

If you're unfamiliar with binary data representation, google the binary number system and bit stream.
void onCommand(CRules@ this, u8 cmd, CBitStream@ params) { if (cmd == this.getCommandID("do_client_sound")) { if (!isClient()) return; // not for server bool play_sound_on_client = params.read_bool(); if (play_sound_on_client) Sound::Play("vine_boom_effect.ogg"); } }



A few important points to consider:
  • Ensure commands are sent from only one side. Use isServer() and isClient() to separate execution logic based on authority.

  • Most client-side code executes simultaneously for all instances of the same object (e.g. knight character) under different players’ control.
    For example, if a player blob gets hit and a command is sent from the client-side, each client processing that event may also send the command.
    To prevent duplicate commands, compare player instances using blob.isMyPlayer(), getLocalPlayer() is blob.getPlayer(), or getLocalPlayerBlob() is blob.

  • After receiving a command, the server re-sends it back to all clients, but not vice-versa.

  • The saferead_ methods, unlike read_ methods, return true if the data was successfully read and not corrupted, and false otherwise.

  • Since the recent update, probably just in staging, you can no longer initialize a CBitStream with the same name as one in parameters inside onCommand() hook. That will crash the game.


    Syncing Data
    At this point, you should be able to write your own networking scripts. However, KAG also provides a built-in method: Sync(string key, bool from_server_to_client), which can sync various data types, including tags.

    I do not recommend relying on this method, as it has several issues. While syncing from client to server is not supported at all, even server-to-client synchronization can fail under certain circumstances, making it unreliable - fail to sync values or corrupt stream of data causing bad deltas.
    Instead, it’s best to handle data synchronization manually by following the structured approach demonstrated above.
Advanced - additional shapes and sectors
Shapes
Due to inability of blobs to handle more than 8 vertices on one shape, you often need to work around it with additional shapes.
Basically, they are the same shapes, except that you can't configure them separately. They all inherit properties from their main shape.
To the best of my understanding there's no limit for the amount of shapes you can add, however removing the main shape (with index 0) will crash your game.

Here, I've added a shape to the drill from previous examples:
void onInit(CBlob@ this) { // ... other code above Vec2f pos = this.getPosition(); Vec2f[] new_shape = { Vec2f(16, -2), Vec2f(32, 2), Vec2f(16, 6) }; this.getShape().AddShape(new_shape); }

To enable shape-view write /g_debug (from 1 to 6) to console.
You can remove the shape with CShape.RemoveShape(1).


Sectors
There are 2 different types of sectors - static and moving. Both are handled by CMap, however moving sectors require a network id of an existing blob:
void onInit(CBlob@ this) { // ... other code above getMap().server_AddMovingSector(Vec2f(-4,-16), Vec2f(4,16), "ladder", this.getNetworkID()); }
Sectors debug view starts from level 2.

Advanced - adding and changing tiles
What are tiles in KAG
Essentially - sprite frames, some misc properties like transparency for water and light, collisions and amount of HP, which is set manually in the corresponding scripts, associated with its current damage-frame.

On the default sprite sheet world.png, most of tiles are separated in row just to simplify initialization in the enum, besides, most tiles have the frames for tile masking and the formulas to calculate a preferable mask.
The mask is a visual change depending on the tiles nearby, all vanilla masks are driven in engine.

Luckily, there are multiple mods with custom tile masking system, most of which are copied from TC though.
I also know Skemonde made a better version of it for his KIWI mod, something like calculating 2 blocks in depth in all directions instead of 1 to get a correct mask.

Generally speaking, you should first investigate and understand other methods before writing your own if you never had a chance to write own tile masking.

Note that vanilla tiles are completely closed from us (except the sprite sheet), and we can't change their logic. The only way remaining is re-create them as our new, custom tiles.

Declaring custom tiles
Firstly, add the tiles you want to world.png. The closest empty index for tiles after all reserved starts at 384.
Each row on the sheet can fit 16 tiles on it, but you can wrap them to anywhere if needed - there is no strict rules where you place the tile sprites, but never change the sprite sheet width, as it breaks vanilla tile masking. You should change the sprite sheet height instead.




1. You need the scripts CustomBlocks.as and LoaderColors.as:
In CustomTiles enum of CustomBlocks script initialize the set of tiles:
enum CustomTiles { tile_scrap = 400, // default tile, the "signature" for mask tiles tile_scrap_v0, // mask, implicitly 401 tile_scrap_v1, // mask, 402 tile_scrap_v2, // and so on tile_scrap_v3, tile_scrap_v4, tile_scrap_v5, tile_scrap_v6, tile_scrap_v7, tile_scrap_v8, tile_scrap_v9, tile_scrap_v10, tile_scrap_v11, tile_scrap_v12, tile_scrap_v13, tile_scrap_v14, tile_scrap_d0 = tile_scrap + 16, // first damaged tile, +16 means it's one line lower on the sprite sheet (also it's an explicit hint to show the index) tile_scrap_d1, // second damaged tile, that is set when _d0 is hit tile_scrap_d2, // and so on tile_scrap_d3, tile_scrap_d4, tile_scrap_d5, tile_scrap_d6 // last damaged tile. If this one is last in the enum, remove the comma }

2. Preferably add this function:
bool isTileScrap(u16 tile) { CMap@ map = getMap(); return tile >= tile_scrap && tile <= tile_scrap_d6; }

3. In colors enum of LoaderColors script initialize a HEX color for your tile like this:
tile_scrap = 0xFFB25324,
You may use ARGB to HEX converter if you like.

4. Optionally add any color to MinimapColors.as following the script logic. This script defines the color of minimap and the preview in the server browser, when you select the server.

5. Now we need the tile spawner in the script BasePNGLoader.as. Add this to the switcher under // Tiles section
case map_colors::tile_scrap: map.SetTile(offset, CMap::tile_scrap); break;

6. In the LoaderUtilities.as you should write a logic for your tiles.
The easiest way is to copy-and paste similar operations from other mods, but I will provide a short explanation.

You have 3 main hooks:
  • onMapTileCollapse() which is called when the tile starts falling

  • server_onTileHit() which is server-side only and is called when a tile was hit. The hook provides a parameter "damage" that you can use to break the tile for more than 1 step, e.g.
    u16 newTile = oldTileType + Maths::Ceil(damage); if (!isTileScrap(newTile)) return CMap::tile_empty; return newTile;

    The hook must return a TileType (u16 pseudonym), which is the index of remaining tile.
    For example, when you hit a dirt tile first time, it always returns _d0 variant. When you break dirt completely (with hitting the last _d tile), it returns a new tile - tile_ground_back.
    Unlike tile_wood and some of other tiles, dirt never applies damage argument.

    Example:
    case CMap::tile_scrap: return CMap::tile_scrap_d0; case CMap::tile_scrap_v0: ... other tile variants inbetween case CMap::tile_scrap_v14: { Vec2f pos = map.getTileWorldPosition(index); map.AddTileFlag(index, Tile::SOLID | Tile::COLLISION); // solid tile flags map.RemoveTileFlag(index, Tile::LIGHT_PASSES | Tile::LIGHT_SOURCE); // solid tile flags for (u8 i = 0; i < 4; i++) {scrap_Update(map, map.getTileWorldPosition(index) + directions);} // update custom mask for nearby tiles

    return CMap::tile_scrap_d0; // return first damaged frame
    }

    case CMap::tile_scrap_d0:
    ... other damaged tiles inbetween
    case CMap::tile_scrap_d5:
    return oldTileType + 1; // return next damage frame

    case CMap::tile_scrap_d6: // remove the tile
    return CMap::tile_empty;[/code]

  • onSetTile() which is called when a tile was created.

    Example:
    case CMap::tile_scrap: { Vec2f pos = map.getTileWorldPosition(index); scrap_SetTile(map, pos); map.AddTileFlag(index, Tile::SOLID | Tile::COLLISION); map.RemoveTileFlag(index, Tile::LIGHT_PASSES | Tile::LIGHT_SOURCE | Tile::WATER_PASSES); if (isClient()) Sound::Play("build_wall.ogg", map.getTileWorldPosition(index), 1.0f, 1.0f); break; }

Advanced - GUI
GUI
Graphic User Interface is another way of "drawing" things in KAG.
It is an independent library and works slightly differently from map and blobs rendering.
The framerate in vanilla version is always 60 FPS, although in staging you can set a custom limit.
  • The hook onRender() is called each frame.
  • Just like other CSprite hooks, onRender() is needed at blob's sprite script list.
  • onRender() won't be called if the blob running it is outside of your screen space
  • onRender(CRules@ this) works same as onTick(CRules@ this) except it has different rate.

    GUI and most rendering features use different coordinate system - your screen, not usual for other objects that utilize map coordinates.
    The screen in question is your view in-game, where Vec2f(0, 0) is the first pixel on it, in the top left corner, and Vec2f(screenwidth, screenheight) - the last.
    GUI::DrawRectangle(Vec2f(300,300), Vec2f(500,500), SColor(0xaaff0000));


    In case when you want to set render position to blob position, you need to interpolate the coordinates from worldspace (the map) to screen space using Driver method getScreenPosFromWorldPos() like in the example:
    Vec2f drillpos = blob.getPosition(); // current position Vec2f old_drillpos = blob.getOldPosition(); // position on previous tick Vec2f localpos = localBlob.getPosition(); // my player position Vec2f old_localpos = localBlob.getOldPosition(); // my player position on previous tick Vec2f pos2d = getDriver().getScreenPosFromWorldPos(blob.getPosition()); Vec2f old_pos2d = getDriver().getScreenPosFromWorldPos(blob.getOldPosition()); // interpolate to framerate to prevent stuttering when we move pos2d = Vec2f_lerp(old_pos2d, pos2d, getInterpolationFactor()); Vec2f local_pos2d = getDriver().getScreenPosFromWorldPos(localBlob.getPosition()); Vec2f old_local_pos2d = getDriver().getScreenPosFromWorldPos(localBlob.getOldPosition()); local_pos2d = Vec2f_lerp(old_local_pos2d, local_pos2d, getInterpolationFactor()); Vec2f tl = pos2d; Vec2f br = local_pos2d; GUI::DrawRectangle(tl, br, SColor(0xaaff0000));

CBlob, CShape, CSprite and other default classes
A lot of not-so-important classes were skipped.

CBlob
Most default and versatile class in KAG. Syncing CBlob's properties incorrectly may lead to bad deltas (desync) or/and crashes.

CSprite
Client-only.
Handles playsound and emitsound methods. Runs spritelayers. SetRelativeZ() is calculated relatively to zero, if SetZ() was not assigned. Tiles have Z index of 500 and background index of -500.
ResetTransform() resets most of its properties.

CSpriteLayer
Client-only.
Inherits most of its CSprite properties. SetRelativeZ is calculated relatively to Z index of its CSprite.
If two sprite layers have same Z index, the latest spawned will render above previous. This rule applies to CSprite as well.

CShape
The secondary part of CBlob. Incorrect shape settings may lead to crash. Static shapes will disable its facingleft management and rotations, unlike gravity.

CPlayer
Most of setters are server-only.
Player instance. The engine will safely sync almost all of it.

CControls
A global function for client-only (calling to its methods server-side may result in memory leaks):
tracks all key inputs
A method of CBlob:
works only with dedicated key inputs like the ones you can rebind in Settings

CAttachment and AttachmentPoint
First serves as a collection of attachment points. Returns null sometimes, if called from CBrain hooks, add null checks there.

CRules
Global instance for the game. onRestart() is called after full map load.
Syncs all properties to client when it connects.
Never automatically resets properties assigned with set_ methods. Avoid spamming too much of them to memory.

CMap
Global instance for the tiles, markers and sectors.

CBrain
Server-only.
Handles pathfinder and some other AI methods.

CMixer
Client-only.
Ambient and background music.

ConfigFile
Parses config files. Used to store long-term data in
Base/Cache/
.
CFileMatcher
Serves as router and file searcher.

CFileImage
Image parser. Used in BasePNGLoader.as.

CMovement
Most likely a legacy remaining, not necessary.

Random
Seeded random. XORRandom(uint from_zero_to_value) is an alternative.
1 Comments
sarikvoly4 1 Feb @ 6:04am 
thx for guide:kag_cool: