Events-based Scripting in CtP2 (part 2)
5.0 - Functions
Function reference - http://apolyton.net/ctp2/modificatio...lic2func.shtml
Functions come in two types, as, I beleive they do in C. Integer functions (int_f) return a number, and void functions (void_f) don't return anything, they just execute some action.
I think in essence, with SLIC as a way to code reactions to events, the void_f functios will be the reaction, and the int_f functions will be a way of narrowing down where the reaction occurs.
5.1 - Code with Functions In
In reality, it is nearly impossible to code anything without using functions, but some code is more based on functions than others.
Going back to this example:
In the emboldened section are the functions GetCityByIndex, CityHasBuilding and AddGold.
GetCityByIndex and AddGold are void_f functions, they do not return any integers, they just react.
AddGold is a simple action function, it adds gold to the player's treasury.
GetCityByIndex is more complicated, though it doesn't return a number, it doesn't act as such. What it does is return a city, in the form of a variable defined beforehand, in this case tmpCity. tmpCity can now be used inside the for loop to act on that city, (after the for loop, it will only apply to the last city found in the for loop (ie when i = player[0].cities - 1).
CityHasBuilding is an int_f function, so is usually used in a if statement. IF(CityHasBuilding(tmpCity, BuildingDB(IMPROVE_GRANARY))) THEN { do something. IF it doesn't have that building, then that reaction does not apply in this case. The int_f has limited the number of cases where the reaction applies, as carried out by the void_f function AddGold.
5.2 - Nested Functions
It is possible to use functions inside other functions to save the trouble of defining and using variables all the time.
eg.
This will get a city belonging to player[0] with a random index number. The random number is chosen from between 0 and player[0]'s amount of cities, so will always return a valid city. The city is then stored as tmpCity.
This saved the bother of storing PlayerCityCount(player[0]) as a variable, then finding random(player[0].cities) and storing that, and then plugging that into the GetCity... function.
5.3 - User-made Functions
As well as the built-in functions, SLIC allows you to define your own functions and use them in handlers. These had me really confused at the start, as I could see loads of code with no event to trigger it, and then functions in handlers which were not listed. A good example of a custom function is Dale GetNearestCity function.
This function is used inside the main Withdraw script handler to find the nearest city that the aeroplane should return to.
Inside a handler, the title of the fnction becomes a function, so putting it into the example handler from above:
That is an example of using a new function as a time and space saving device inside a handler. Often the function will be a small part of the code that is primarily the reaction to the event.
Occassionally though, the function is more important, and the handlers are needed only to trigger it at certain times.
5.4 - Function-Based Code
One of the simplest example of this type of code is Locutus Capitol Code for the MedMod.
He uses a function to rebuild a capitol, and the handlers merely as triggers to run the function.
and the handlers that fire it when a capitol is destroyed:
Using functions separate from the main code means that the code is only written out once, rather than once for each event. This makes the file shorter, easier to manage and debug, and not as spacious for uploading or downloading.
5.5 - Events as Functions
As well as using events to trigger reaction code, events can be forced to happen, just as functions can cause things to happen.
eg. Taken a bit from example handler.
This forces the game to recognise that the fortify event has happened, as if someone pressed the F button.
Most events in the events listing can be used like this. The ones that can't are the ones where one of the variables has a GEA_ prefix in the list. These variables are paths, and are too complicated to define in SLIC. (http://apolyton.net/ctp2/modificatio...icevents.shtml)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~
6.0 MessageBoxes
Messageboxes come in two forms. Mesageboxes and alertboxes. Messageboxes can be left open, while the game continues around them. Alertboxes force play to be stopped until you have responded to them.
A typical messagebox in SLIC looks like this:
Alertboxes are exactly the same as messageboxes, except instead of having the word messagebox where it is bold, have the word alertbox.
Title, text and button text should be stored in scen_str.txt in ctp2_data/language/gamedata directory (Without the ID_ prefix, that is just to show the SLIC what it is)
To make a message appear, use the message function in the handler.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~
7.0 Mod_ functions
The wierdest of all SLIC, these are functions that do not have to be triggered, but apply all the time in the game. There are theoretically eight, but only four have obvious effects. These are:
They do exactly as they suggest, they limit which techs, wonders, buildings and units are available to players and cities.
You can use code (mostly if statements I should think) to limit to where the rules apply. If in doubt, copy a known format. The Alexander the Great scenario has a basic example of limitations to a tech tree, and the MedMod2 restricts the use of some units. I think the only place that uses the mod_CanCityBuildBuilding function is my Mars 2020 scenario (currently in beta testing), so here is the code for that.
Start with deciding which building you want to limit the use of, then decide who you are limiting the building to or from, and why.
If anyone wants to experiment with the other four, they are:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~
8.0 End Note
As I said at the beginning, this is not necessarily a good way to learn SLIC, but it is in theory the way I learned. In actuality, I learned by doing. Hack as much code from other places as you can. Some of the functions are buggy, some don't work, some have limitations. Read around the Apolyton Mods forum right back to the beginning, and download everything you can. (If you can understand BlueO's city expansion then you're there!) Read as much SLIC as you can, and try to figure out why their code works. If your's doesn't, try for a workaround, model your code on other people's, and start a help thread in the forum, we the SLICers will help, 'cos we all had difficulties to start with.
So...
Before you code, have a method
Work though the method, be flexible, and willing to mix methods to get a result.
The best debugger is the game itself. Put DebugSlic to Yes in userprofile.txt to get rid of errors.
Alt-Tab out, fix the bug, and /reloadslic. Repeat ad nauseum
When it goes right, no matter how clumsy it looks in relation to other code, its a great feeling
And have fun!
Ben Weaver (aka The Immortal Wombat)
24/11/01
attachment is a text version of this, with vB and HTML tags included.
5.0 - Functions
Function reference - http://apolyton.net/ctp2/modificatio...lic2func.shtml
Functions come in two types, as, I beleive they do in C. Integer functions (int_f) return a number, and void functions (void_f) don't return anything, they just execute some action.
I think in essence, with SLIC as a way to code reactions to events, the void_f functios will be the reaction, and the int_f functions will be a way of narrowing down where the reaction occurs.
5.1 - Code with Functions In
In reality, it is nearly impossible to code anything without using functions, but some code is more based on functions than others.
Going back to this example:
Code:
HandleEvent(MoveUnits) 'kill_the_unit' pre { unit_t tmpUnit; // define variable tmpUnit = unit[0]; // define what it actually is player[0] = unit[0].owner; // name the unit's owner (it is not implicit in the event) if(player[0].cities > 10){ // if statement. IF the unit's owner has 10 cities... KillUnit(tmpUnit); // if 'yes', then do function. Kill the unit. } // close if statement elseif(player[0].cities < 3){ // ELSEIF the unit's owner has less than 3 cities... KillUnit(tmpUnit); // if 'yes', then do function. Kill the unit. int_t i; // define counter variable for(i = 0; i < player[0].cities; i = i + 1){ // for each of player[0]'s cities... city_t tmpCity; // define GetCityByIndex(player[0], i, tmpCity); // Find the city currently being dealt with if(CityHasBuilding(tmpCity, BuildingDB(IMPROVE_SILO))){ // if city has silo AddGold(player[0], 5000); // give 5000 compensation } elseif(CityHasBulding(tmpCity, BuildingDB(IMPROVE_GRANARY))){ // otherwise if it has a granary AddGold(player[0], 3000); // give 3000 gold } else { AddGold(player[0], 1000); // otherwise, give just 1000 } } // close for loop } // close elseif statement else{ // ELSE. (none of the above)... Heal(tmpUnit); // if its still there, then heal the unit. } // close else loop } // close handler
GetCityByIndex and AddGold are void_f functions, they do not return any integers, they just react.
AddGold is a simple action function, it adds gold to the player's treasury.
GetCityByIndex is more complicated, though it doesn't return a number, it doesn't act as such. What it does is return a city, in the form of a variable defined beforehand, in this case tmpCity. tmpCity can now be used inside the for loop to act on that city, (after the for loop, it will only apply to the last city found in the for loop (ie when i = player[0].cities - 1).
CityHasBuilding is an int_f function, so is usually used in a if statement. IF(CityHasBuilding(tmpCity, BuildingDB(IMPROVE_GRANARY))) THEN { do something. IF it doesn't have that building, then that reaction does not apply in this case. The int_f has limited the number of cases where the reaction applies, as carried out by the void_f function AddGold.
5.2 - Nested Functions
It is possible to use functions inside other functions to save the trouble of defining and using variables all the time.
eg.
Code:
city_t tmpCity; GetCityByIndex(player[0], random(PlayerCityCount(player[0])), tmpCity);
This saved the bother of storing PlayerCityCount(player[0]) as a variable, then finding random(player[0].cities) and storing that, and then plugging that into the GetCity... function.
5.3 - User-made Functions
As well as the built-in functions, SLIC allows you to define your own functions and use them in handlers. These had me really confused at the start, as I could see loads of code with no event to trigger it, and then functions in handlers which were not listed. A good example of a custom function is Dale GetNearestCity function.
Code:
int_f GetDistanceNearestCity(location_t theLoc, int_t thePlayer) { // this is an int_f function needing an input of location and player // definitions cut for space... tmpPlayer = thePlayer; tmpLoc = theLoc; min = 10000; city = 0; cities = Cities(tmpPlayer); for(i = 0; i < cities; i = i + 1) { GetCityByIndex(tmpPlayer, i, tmpCity); val = SquaredDistance(tmpCity.location, tmpLoc); if(val < min) { min = val; city = i; } } return city; // this is the integer that the function returns at the end. } // It is the index of thePlayer's closest city.
Inside a handler, the title of the fnction becomes a function, so putting it into the example handler from above:
Code:
HandleEvent(MoveUnits) 'kill_the_unit' pre { unit_t tmpUnit; tmpUnit = unit[0]; player[0] = unit[0].owner; if(player[0].cities > 10){ KillUnit(tmpUnit); } elseif(player[0].cities < 3){ KillUnit(tmpUnit); int_t i; for(i = 0; i < player[0].cities; i = i + 1){ city_t tmpCity; GetCityByIndex(player[0], i, tmpCity); if(CityHasBuilding(tmpCity, BuildingDB(IMPROVE_SILO))){ AddGold(player[0], 5000); } else { AddGold(player[0], 1000); } } // Once all cities have been done.. int_t tmpIndex; tmpIndex = GetNearestCity(unit[0].location, player[0]); // find the nearest home city to the unit GetCityByIndex(player[0], tmpIndex, tmpCity); // store that city CreateUnit(player[0], unit[0].type, tmpCity.location, 0); // create a unit of the same type there } else{ Heal(tmpUnit); } }
Occassionally though, the function is more important, and the handlers are needed only to trigger it at certain times.
5.4 - Function-Based Code
One of the simplest example of this type of code is Locutus Capitol Code for the MedMod.
He uses a function to rebuild a capitol, and the handlers merely as triggers to run the function.
Code:
// function that actually finds a replacement city for the capital and makes it build a new one int_f MM2_CreateCapital(int_t thePlayer) { city_t tmpCity; city_t largestCity; int_t i; int_t size; int_t tmpPlayer; tmpPlayer = thePlayer; player[0] = tmpPlayer; if (player[0].cities > 1) { // if player has more cities size = -1; for (i = 0; i < player[3].cities; i = i + 1) { // cycle through all cities GetCityByIndex(tmpPlayer, 0, tmpCity); if (tmpCity.population > size && !CityHasBuilding(tmpCity, "IMPROVE_CAPITOL")) { // check for capitol needed since this is executed pre capture, so old capital is evaluated as well if (IsWonderAtHead(tmpCity) == -1) { // check for wonder at head of build queue is needed since ClearBuildQueue doesn't clear queues with wonders at head largestCity = tmpCity; // store city size = largestCity.population; } } } if (size > -1) { // if a city was found ClearBuildQueue(largestCity); // clear existing buildqueue AddBuildingToBuildList(largestCity, BuildingDB(IMPROVE_CAPITOL)); // start building a Capitol Event:BuyFront(largestCity); // (try to) rush buy it } } }
Code:
// detect the capture of a Capital and call CreateCapital function HandleEvent(CaptureCity) 'MM2_CapitalCaptured' pre { int_t tmpPlayer; city_t tmpCity; tmpCity = city[0]; tmpPlayer = tmpCity.owner; player[3] = tmpPlayer; if (CityHasBuilding(tmpCity, "IMPROVE_CAPITOL")) { // if city is capital/has capitol if (!IsHumanPlayer(tmpPlayer)) { // if player isn't human MM2_CreateCapital(tmpPlayer); // find new capital } } } //ditto for KillCity event, GiveCity event, DisbandCity event, and CreatePark event.
5.5 - Events as Functions
As well as using events to trigger reaction code, events can be forced to happen, just as functions can cause things to happen.
eg. Taken a bit from example handler.
Code:
int_t tmpIndex; tmpIndex = GetNearestCity(unit[0].location, player[0]); // find the nearest home city to the unit GetCityByIndex(player[0], tmpIndex, tmpCity); // store that city CreateUnit(player[0], unit[0].type, tmpCity.location, 0, tmpUnit); // create tmpUnit of the same type there. Event:EntrenchUnit(tmpUnit); // Use entrench event to fortify it.
Most events in the events listing can be used like this. The ones that can't are the ones where one of the variables has a GEA_ prefix in the list. These variables are paths, and are too complicated to define in SLIC. (http://apolyton.net/ctp2/modificatio...icevents.shtml)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~
6.0 MessageBoxes
Messageboxes come in two forms. Mesageboxes and alertboxes. Messageboxes can be left open, while the game continues around them. Alertboxes force play to be stopped until you have responded to them.
A typical messagebox in SLIC looks like this:
Code:
messagebox 'kill_unit_or_not' { // definition messagebox, name, open bracket Show(); // otherwise it wouldn't show Title(ID_TITLE); // message title Text(ID_TEXT); // message text Button(ID_YES){ // button Kill(); // effect button has. Kills text box KillUnit(unit[0]) // kills the unit } // end button effect Button(ID_NO){ // other button Kill(); // only kills the text box } // end button effect } // close
Title, text and button text should be stored in scen_str.txt in ctp2_data/language/gamedata directory (Without the ID_ prefix, that is just to show the SLIC what it is)
To make a message appear, use the message function in the handler.
Code:
HandleEvent(MoveUnits) 'kill_the_unit' pre { unit_t tmpUnit; tmpUnit = unit[0]; player[0] = unit[0].owner; if(player[0].cities > 10){ KillUnit(tmpUnit); } elseif(player[0].cities < 3){ KillUnit(tmpUnit); int_t i; for(i = 0; i < player[0].cities; i = i + 1){ city_t tmpCity; GetCityByIndex(player[0], i, tmpCity); if(CityHasBuilding(tmpCity, BuildingDB(IMPROVE_SILO))){ AddGold(player[0], 5000); } else { AddGold(player[0], 1000); } } // Once all cities have been done.. int_t tmpIndex; tmpIndex = GetNearestCity(unit[0].location, player[0]); // find the nearest home city to the unit GetCityByIndex(player[0], tmpIndex, tmpCity); // store that city CreateUnit(player[0], unit[0].type, tmpCity.location, 0); // create a unit of the same type there } else{ message(player[0], 'kill_unit_or_not'); // give a choice whether or not to kill it } }
7.0 Mod_ functions
The wierdest of all SLIC, these are functions that do not have to be triggered, but apply all the time in the game. There are theoretically eight, but only four have obvious effects. These are:
Code:
int_f mod_CanPlayerHaveAdvance(int_t thePlayer, int_t theAdvance) {
Code:
int_f mod_CanCityBuildBuilding(city_t theCity, int_t theBuilding) {
Code:
int_f mod_CanCityBuildUnit(city_t theCity, int_t theUnit) {
Code:
int_f mod_CanCityBuildWonder(city_t theCity, int_t theWonder) {
You can use code (mostly if statements I should think) to limit to where the rules apply. If in doubt, copy a known format. The Alexander the Great scenario has a basic example of limitations to a tech tree, and the MedMod2 restricts the use of some units. I think the only place that uses the mod_CanCityBuildBuilding function is my Mars 2020 scenario (currently in beta testing), so here is the code for that.
Code:
int_f mod_CanCityBuildBuilding(city_t theCity, int_t theBuilding) { int_t tmpBuilding; tmpBuilding = theBuilding; tmpCity = theCity; city[0] = tmpCity; if(tmpBuilding == BuildingDB(IMPROVE_SOLAR_PLANT)){ if(g.year >= 48){ return 1; } else { return 0; } } elseif(tmpBuilding == BuildingDB(IMPROVE_HOSPITAL) || tmpBuilding == BuildingDB(IMPROVE_PUBLISHING_HOUSE)) { if(G.YEAR >= 36){ return 1; } else { return 0; } } elseif(tmpBuilding == BuildingDB(IMPROVE_ORBITAL_LABORATORY) ) { if(G.YEAR >= 24){ return 1; } else { return 0; } } elseif(tmpBuilding == BuildingDB(IMPROVE_ACADEMY)){ if(g.year >= 12){ return 1; } else { return 0; } } elseif(tmpBuilding == BuildingDB(IMPROVE_AQUEDUCT)){ tmpNum = city[0].population; if(tmpNum >= 3){ return 1; } else { return 0; } } elseif(tmpBuilding == BuildingDB(IMPROVE_COMPUTER_CENTER)){ tmpNum = city[0].population; if(tmpNum >= 3 && g.year >= 6){ return 1; } else { return 0; } } else { return 1; } }
If anyone wants to experiment with the other four, they are:
Code:
mod_CityHappiness mod_UnitAttack mod_UnitRangedAttack mod_UnitDefense
8.0 End Note
As I said at the beginning, this is not necessarily a good way to learn SLIC, but it is in theory the way I learned. In actuality, I learned by doing. Hack as much code from other places as you can. Some of the functions are buggy, some don't work, some have limitations. Read around the Apolyton Mods forum right back to the beginning, and download everything you can. (If you can understand BlueO's city expansion then you're there!) Read as much SLIC as you can, and try to figure out why their code works. If your's doesn't, try for a workaround, model your code on other people's, and start a help thread in the forum, we the SLICers will help, 'cos we all had difficulties to start with.
So...
Before you code, have a method
Work though the method, be flexible, and willing to mix methods to get a result.
The best debugger is the game itself. Put DebugSlic to Yes in userprofile.txt to get rid of errors.
Alt-Tab out, fix the bug, and /reloadslic. Repeat ad nauseum
When it goes right, no matter how clumsy it looks in relation to other code, its a great feeling
And have fun!
Ben Weaver (aka The Immortal Wombat)
24/11/01
attachment is a text version of this, with vB and HTML tags included.
Comment