3 In this tutorial we are going to create a Xonotic mutator that will print text when the player spawns.
5 ## Step 1: Getting source code
7 First, you will need to get the [source code of Xonotic](Repository_Access). Once you've cloned the repository and built the game successfully, we can start adding our code.
9 Xonotic is built on top of DarkPlaces engine which in turn was forked from the Quake 1 engine. The game code is written in QuakeC. If you have experience with C or C++, it will be no problem to adapt to QuakeC. All QuakeC code is located in `xonotic/data/xonotic-data.pk3dir/qcsrc/` directory.
11 Here is the outline of some subdirectories:
13 * `client` - this directory contains client-side code.
14 * `common` - this directory contains the code that is shared between client and server.
15 * `dpdefs` - this directory contains declarations that are used by the engine - a glue between the engine and the game.
16 * `lib` - this directory contains "libraries" - pieces of reusable code that are not specific to Xonotic.
17 * `menu` - this directory contains client-side menu code.
18 * `server` - this directory contains server-side code.
19 * `tools` - this directory contains some useful shell scripts.
21 ## Step 2: Creating a separate git branch
23 Xonotic uses git as its version control system. It is recommended to make your changes on a separate branch. There are few advantages of doing so. First, your code will not interfere with a main branch and you can quickly switch between vanilla Xonotic and your modded Xonotic. Second, if there is a breaking change in the master branch, it won't break your code until you will request manual merge of your code. Finally, this is the only way for your code to be officially added to the game.
25 Since we will only be modding the QuakeC part of the game, we only need to create our branch of `xonotic-data.pk3dir` repository. You should name your branch `<Your name>/<your feature>`.
27 xonotic$ cd data/xonotic-data.pk3dir
28 xonotic/data/xonotic-data.pk3dir$ git checkout -b Lyberta/HelloWorld
30 ## Step 3: Adding your code
32 Regular mutators are located in `common/mutators/mutator` directory, let's create a new directory called `helloworld`. Now, the prefixes of files determine whether the code is included in different QuakeC virtual machines. Files that start with `cl_` are only compiled into client-side code and files that start with `sv_` are only compiled into server-side code. Our mutator will be fully server-side so let's create a file called `sv_helloworld.qc`.
34 Now we need to register our mutator, open `sv_helloworld.qc` and add the following text:
36 REGISTER_MUTATOR(helloworld, cvar("g_helloworld"));
38 `helloworld` is the name of our mutator and `g_helloworld` is the console variable that will enable our mutator. Now we need to make our code execute when player spawns. To do that, Xonotic defines hooks that can be used by mutators. Server-side hooks are defined in `qcsrc/server/mutators/events.qh`. If we open this file and search for `PlayerSpawn`, we will find this:
40 #define EV_PlayerSpawn(i, o) \
41 /** player spawning */ i(entity, MUTATOR_ARGV_0_entity) \
42 /** spot that was used, or NULL */ i(entity, MUTATOR_ARGV_1_entity) \
44 MUTATOR_HOOKABLE(PlayerSpawn, EV_PlayerSpawn);
46 This code defines our hook, we can use by writing the following code in `sv_helloworld.qc`:
48 MUTATOR_HOOKFUNCTION(helloworld, PlayerSpawn)
52 Now we have created our hook but its body is empty, we need to write some code between curly braces. How to find which functions we need to call? Using grep. Grep is used to find text inside files and is essential in exploring Xonotic codebase. The syntax we need is `grep <text> -iRn`. Let's open the terminal in `qcsrc` directory and search for `PrintToChat`:
54 xonotic/data/xonotic-data.pk3dir/qcsrc$ grep PrintToChat -iRn
56 We will get something like this:
58 server/player.qh:18:void PrintToChat(entity player, string text);
59 server/player.qh:25:void DebugPrintToChat(entity player, string text);
60 server/player.qh:30:void PrintToChatAll(string text);
61 server/player.qh:36:void DebugPrintToChatAll(string text);
62 server/player.qh:42:void PrintToChatTeam(int teamnum, string text);
63 server/player.qh:49:void DebugPrintToChatTeam(int teamnum, string text);
65 Good, we have found our function. Let's add it to our mutator:
67 MUTATOR_HOOKFUNCTION(helloworld, PlayerSpawn)
69 PrintToChatAll("Hello world!");
72 ## Step 4: Adding code to the build system
74 Alright, that should do it. Now we need to instruct the build system to build our code. We do this by running `genmod.sh` script from the `tools` directory inside `qcsrc` directory.
76 xonotic/data/xonotic-data.pk3dir/qcsrc$ ./tools/genmod.sh
78 You can now see that there 2 new `_mod` files inside our `helloworld` directory and if you do `git diff`, you can see that other `_mod` files now contain our new mutator.
80 ## Step 5: Building the code
82 Now we need to build the QuakeC code. It is done using the `all` script:
84 xonotic$ ./all compile -qc -r
86 ## Step 6: Adjusting config files
88 The last thing we need to do is to create our console variable `g_helloworld` in a configuration file. The best file for it would be `mutators.cfg`:
94 The best way to test code changes is to run a dedicated server. You need to put `g_helloworld 1` in your `server.cfg`.
96 Now, start your server, connect to it and spawn. You should see the "Hello world!" message in the chat.
98 ## Step 8: Committing changes
100 Now, if everything works you can commit your mutator into the git repository:
102 xonotic/data/xonotic-data.pk3dir$ git add .
103 xonotic/data/xonotic-data.pk3dir$ git commit -m "Added HelloWorld mutator. Yay!"
105 Now you can switch between vanilla Xonotic and your modded version. Type:
107 xonotic/data/xonotic-data.pk3dir$ git checkout master
109 to switch to vanilla Xonotic and type
111 xonotic/data/xonotic-data.pk3dir$ git checkout Lyberta/HelloWorld
113 to switch to your modded version.
117 ## Printing to specific player's chat
119 If you've tested your mutator with bots or other people, you may have noticed that `Hello world!` is being printed to everyone when any player spawns. Let's make it so it is only printed to the player that just spawned. For that we need to know the player entity. Thankfully, our hook has it, let's look at it again:
121 #define EV_PlayerSpawn(i, o) \
122 /** player spawning */ i(entity, MUTATOR_ARGV_0_entity) \
123 /** spot that was used, or NULL */ i(entity, MUTATOR_ARGV_1_entity) \
125 MUTATOR_HOOKABLE(PlayerSpawn, EV_PlayerSpawn);
127 As you can see, it has player entity as the variable at index 0. We can access it using `M_ARGV` macro.
129 entity player = M_ARGV(0, entity);
131 And then we can use `PrintToChat` function.
133 MUTATOR_HOOKFUNCTION(helloworld, PlayerSpawn)
135 entity player = M_ARGV(0, entity);
136 PrintToChat(player, "Hello world!");
139 ## Greeting the player personally
141 But we can do more, we can greet the player with their name. Player name is stored in the `netname` field of their entity. However, we also need to concatenate the `Hello` string with the player's name. We can use the `strcat` function to do this.
143 MUTATOR_HOOKFUNCTION(helloworld, PlayerSpawn)
145 entity player = M_ARGV(0, entity);
146 string greeting = strcat("Hello, ", player.netname);
147 PrintToChat(player, greeting);
152 MUTATOR_HOOKFUNCTION(helloworld, PlayerSpawn)
154 entity player = M_ARGV(0, entity);
155 PrintToChat(player, strcat("Hello, ", player.netname));
158 ## Modifying hook variables
160 `M_ARGV` macro can also be used to modify variables. For example, let's make our mutator double all damage. There is a `Damage_Calculate` hook:
162 #define EV_Damage_Calculate(i, o) \
163 /** inflictor */ i(entity, MUTATOR_ARGV_0_entity) \
164 /** attacker */ i(entity, MUTATOR_ARGV_1_entity) \
165 /** target */ i(entity, MUTATOR_ARGV_2_entity) \
166 /** deathtype */ i(float, MUTATOR_ARGV_3_float) \
167 /** damage */ i(float, MUTATOR_ARGV_4_float) \
168 /** damage */ o(float, MUTATOR_ARGV_4_float) \
169 /** mirrordamage */ i(float, MUTATOR_ARGV_5_float) \
170 /** mirrordamage */ o(float, MUTATOR_ARGV_5_float) \
171 /** force */ i(vector, MUTATOR_ARGV_6_vector) \
172 /** force */ o(vector, MUTATOR_ARGV_6_vector) \
174 MUTATOR_HOOKABLE(Damage_Calculate, EV_Damage_Calculate);
176 As you can see, there are a lot of variables, we need only `damage`. It has index 4.
178 MUTATOR_HOOKFUNCTION(helloworld, Damage_Calculate)
180 float damage = M_ARGV(4, float);
182 M_ARGV(4, float) = damage;
187 MUTATOR_HOOKFUNCTION(helloworld, Damage_Calculate)
189 M_ARGV(4, float) *= 2;