CB Script

Discuss the new AI features ("NoAI") introduced into OpenTTD 0.7, allowing you to implement custom AIs, and the new Game Scripts available in OpenTTD 1.2 and higher.

Moderator: OpenTTD Developers

Post Reply
Aphid
Traffic Manager
Traffic Manager
Posts: 168
Joined: 16 Dec 2011 17:08

CB Script

Post by Aphid »

Been working on a CB script and having way too much fun with it.
Better late than never though.

Anyway, by using the built-in functions I can determine quite easily how many passengers, mail, goods are delivered to a town. Same with food/water since TTD tracks that. Idem for any custom cargo with the correct flags set. (So ECS Tourists are treated as passengers and so on)

Toys, coal, and valuables are a different matter though.
Trying to think of a way I thought to loop through all industries on the map once per month, adding the 'cargo in storage' amount to the custom data field in the custom town object responsible for keeping track of that kind of cargo. I.e. when it finds a power plant it looks at the amount of coal delivered to it so far on the last day of the month. That gets added to town.cargo_supplied[6] or somesuch. There are a number of problems with this approach though.
1: It doesn't work.
Either the town's 'influence area' isn't what I think it is (any square with local authority: <TOWNNAME>), or an industry's location isn't what I think it is(the square I click on when the fund button is pressed) (these two are unlikely to be true), or the game does somehow not keep track of the data I want in the way I think it does, or seems at least clear from the API.
On my setup, the Log.Info is never called, if I put an if() around it for a specific town and build an industry in that town!

Code: Select all

local inds = GSIndustryList();
	local loc = null;
	foreach(i, _ in inds) 
		{
		loc = GSIndustry.GetLocation(i);
		foreach(t, _ in this.towns)
			{
		if(GSTown.IsWithinTownInfluence((this.towns[t]).id, loc) == true)
				{
				Log.Info("Industry found in town with this much coal: "+GSIndustry.GetStockpiledCargo(i, COAL),Log.LVL_INFO);
			if(GSIndustry.IsCargoAccepted(i, COAL) == GSIndustry.CAS_ACCEPTED && GSIndustry.GetStockpiledCargo(i, COAL) >= 0)
				{(this.towns[t]).AddAdvancedCargo(GSIndustry.GetStockpiledCargo(i, COAL),0,0);}
			if(GSIndustry.IsCargoAccepted(i, TOYS) == GSIndustry.CAS_ACCEPTED && GSGame.GetLandscape() == GSGame.LT_TOYLAND && GSIndustry.GetStockpiledCargo(i, TOYS) >= 0)
				{(this.towns[t]).AddAdvancedCargo(0,GSIndustry.GetStockpiledCargo(i, TOYS),0);}
			if(GSIndustry.IsCargoAccepted(i, VALUABLES) == GSIndustry.CAS_ACCEPTED && GSGame.GetLandscape() != GSGame.LT_TOYLAND && GSIndustry.GetStockpiledCargo(i, VALUABLES) >= 0)
				{(this.towns[t]).AddAdvancedCargo(0,0,GSIndustry.GetStockpiledCargo(i, VALUABLES));}				
				}
			}
		}
note: The code compiles if there exists a class 'X' of which an array towns is made. X has a method AddAdvancedCargo. It also has a public member id holding a TownID.

2: Cargo delivered on the last day of the month may or may not count towards the goal.
- This is annoying to say the least.

3: It's needlessly long. The algorithm takes O(n^2) operations, where n is the number of towns/industries, iff GetStockpiledCargo is constant. If that's instead f(n), then it's O(f(n)n^2) ops. (I know addadvancedcargo IS constant-time).

Ideally I would want to replace this section of code with something equivalent that's faster and takes into account the last day. Something like having the functions int32 GSIndustry.GetDeliveredCargo(industry_id, cargotype) and TownID GSIndustry.GetLocalAuthority(industry_id).
The former function simply gets the amount of <whatever good> is delivered to the industry last month, and the latter gets me in which town the industry is. It'd return -1 or NULL if it failed ofc.
With these two functions one could make it far easier, you would just need to keep a conversion table between town id's and in which places these are in my array inside this function. Seems okay enough.

One more question,a little simpler: In C++ I can do this:

Code: Select all

class f{
static int k;
inline int thefunction(int g){return g + k;}
}
f::k = GetSetting();
So GetSetting is called only once.
What would be the script equivalent?

And now the final question:
At the beginning of each month, I use a trick to grow the town. I set the 'grow every X days' to some grotesque number, like X = 999,999. And then use GSTown.ExpandTown(TownID id, int32 amt) to grow it. Mostly because I'm using some mathematics to figure out how much I want a town to grow depending on cargo input. At least those work perfectly :D . Simply because I want a static (= constant) conversion of extra cargo delivery above that required to maintain town size to extra people, with the exception of cargoes 'NYR' class. Gameplay balance thing.
The question is: How do I SHRINK a town?
E.g. I want to remove n randomly determined houses from it, where n is an integer,via gamescript.

This is important to prevent people that will stockpile goods to get a town (temporarily) bigger than their normal network would allow it to be.
Last edited by Aphid on 05 Oct 2012 13:10, edited 1 time in total.
User avatar
planetmaker
OpenTTD Developer
OpenTTD Developer
Posts: 9432
Joined: 07 Nov 2007 22:44
Location: Sol d

Re: CB Script

Post by planetmaker »

Aphid wrote: The question is: How do I SHRINK a town?
Simply bulldoze houses and roads.

As to your other questions (I'm not much of a script guy): maybe looking at NoCarGoal and SiliconValley game scripts might give you some hints on how to approach the accounting of cargos.
Aphid
Traffic Manager
Traffic Manager
Posts: 168
Joined: 16 Dec 2011 17:08

Re: CB Script

Post by Aphid »

Looked into Silicon valley.

The script uses calls to GSCargoMonitor.GetIndustryPickupAmount() to determine goods picked up.
The script seems to determine the town by simply using GetClosestTown(Tileindex).

I can't use either, of course :( .

First off, if using GetClosestTown, industries I would not consider to be 'in' a town suddenly are. (power plants etc.). In order to give a city power, a power plant should be reasonably close to it. For my definition of reasonably close I used the town's influence area. Instead, I could use GetClosestTown, but also determine distance to this town (via town.getdistancetotile function), and then use a formula incorporating town's size to determine whether a tile is inside a town or not. Two problems would be: a: properly communicating that to the user (I would need a callback on a chat/console command to be honest, or something that gets into the tooltip of a tile), and b: properly integrating this with weirdly shaped town sizes and sized towns as well as building newGRFs which might have lesser or greater occupancy/tile levels.
Alternatively I could use a constant, say, '25'. Of course that'd be a setting in the script. In that case this function is great.

Secondly, if I use GetClosestTown, this might not necessarily be the town whose influence area the factory is in. If I grow a town in a cluster of small towns from 200 to, say, 40,000 inhabitants, it might happen that the large town could swallow nearby smaller towns. It will build houses within the smaller towns' influence area, and claim it as its own. (I tested this, it's really nice how OTTD works in that respect). Unfortunately, that means there can be tiles closer to town B, but which are in town A's influence area!

Third: In some corner cases, if going for a constant, where I would like to allow at least 25 as a maximum setting, a square could count towards more than one town. Since the minimum town distance can be set to less than 50 by the user!

I would love to use GSCargoMonitor though, if I knew what it was. There's no documentation on it in the API:

http://nogo.openttd.org/api/1.2.2/annotated.html

Not in the list :wink:



Edit: For removing houses, do you mean a process along the lines of (in pseudocode):
- Define DENSITY=80;. << Expected :highest possible: town density.
- Let n be the amt. of houses to remove. Set k=0. Set r=0.
- Get the center tile in a town
- If n is too big (say, n > 200), nest this function. Call RemoveHouses(200), RemoveHouses(n - 200).
- while (k<n)
- r++
- If r is huge (say, r > 2,000), break;
- Get a random tile reasonably close to it, but further if the town has a certain size, using S = min(5, sqrt(N) / (4*DENSITY)) as radius.
- Check if there's anything on the tile I can't bulldoze (road, anything a company owns). If so, continue;.
- Check if the tile contains a house. If not so, continue;.
- Bulldoze the tile. k++.
Last edited by Aphid on 05 Oct 2012 14:00, edited 2 times in total.
Yexo
Tycoon
Tycoon
Posts: 3663
Joined: 20 Dec 2007 12:49

Re: CB Script

Post by Yexo »

Aphid wrote:I would love to use GSCargoMonitor though, if I knew what it was. There's no documentation on it in the API:

http://nogo.openttd.org/api/1.2.2/annotated.html

Not in the list :wink:
It was added after 1.2, see http://nogo.openttd.org/api/trunk/annotated.html
Aphid
Traffic Manager
Traffic Manager
Posts: 168
Joined: 16 Dec 2011 17:08

Re: CB Script

Post by Aphid »

Seems that function fixes things nicely.

Even better so, I can now rewrite my script so that it can support Industry newGRFs like FIRS and ECS.

One thing does pop up with that though, not that it's a big problem but...


How would I, in the settings menu of my Script (the configure button) add a setting that includes, in its description, one of the names of the cargoes?

E.g. a setting that says "Amount of passengers required per 1,000 inhabitants".
And if some newGRF changed passengers to bunnies it would then state:
"Amount of bunnies required per 1,000 inhabitants". (No animal lovers in that town).
Using

Code: Select all

"beginning of string" + GSCargo.GetCargoLabel() + "rest of string"
crashes. Error unknown.
Yexo
Tycoon
Tycoon
Posts: 3663
Joined: 20 Dec 2007 12:49

Re: CB Script

Post by Yexo »

You can't. The cargoes are only known after starting the game since they depend on the NewGRFs that are used. As such in the main menu the cargoes are unknown so you can't display them via the API.
User avatar
Zuu
OpenTTD Developer
OpenTTD Developer
Posts: 4553
Joined: 09 Jun 2003 18:21
Location: /home/sweden

Re: CB Script

Post by Zuu »

Aphid wrote:One more question,a little simpler: In C++ I can do this:
Code:

class f{
static int k;
inline int thefunction(int g){return g + k;}
}
f::k = GetSetting();


So GetSetting is called only once.
What would be the script equivalent?
In Squirrel static class variables are read-only:
squirrel manual wrote:Static variables
Squirrel's classes support static member variables. A static variable shares its value between all instances of the class. Statics are declared by prefixing the variable declaration with the keyword static; the declaration must be in the class body.
Note
Statics are read-only.

Code: Select all

class Foo {
	constructor()
	{
		//..stuff
	}
	name = "normal variable";
	//static variable
	static classname = "The class name is foo";
};
But you could store your static value in a global highly unique variable. In SuperLib I use the trick of highly unique names to make up for the missing namespaces in Squirrel as well as inability to store cached values in static class members. For example at the bottom of helper.nut in SuperLib there is this declaration of "private" cache vars:

Code: Select all

// Private static variable - don't touch (read or write) from the outside.
_SuperLib_Helper_private_pax_cargo <- -1;
_SuperLib_Helper_private_mail_cargo <- -1;

_SuperLib_Helper_private_town_accepted_cargo_list <- null;
_SuperLib_Helper_private_town_produced_cargo_list <- null;
However, due to all code living in the same scope, if you use SuperLib, and would have a variable with one of these names, you would get a conflict. So pick a reasonable unique name for your cache vars as they will live in the global scope.

If it is just your GS, then you don't have to be as extreme as in my case when it is a library that may be used by several different AI/GS developers.
My OpenTTD contributions (AIs, Game Scripts, patches, OpenTTD Auto Updater, and some sprites)
Junctioneer (a traffic intersection simulator)
Aphid
Traffic Manager
Traffic Manager
Posts: 168
Joined: 16 Dec 2011 17:08

Re: CB Script

Post by Aphid »

I wouldn't really have a problem with the read only as the settings are mostly set once the game starts. Though putting them as a private class variable would be even better, right? The example would then be;

Code: Select all

class f{
constructor {}
static k = GSController.GetSetting("MySetting");
function thefunction(g);
}
function f::thefunction{return g + k;}
Where k gets written to exactly once.

So, this Script now supports industry newGRF.

I also rewrote other parts of it, after looking at the much prettier things other people made.

Here's the code in the first post that gave me the original problem again:

Code: Select all

	function CityBuilder::CreateIndustryList()
	{
	local result = [];
		/* Make a squirrel array of all industries.
	 */
	local the_industries = GSIndustryList();
	foreach(i in the_industries)
	{
	foreach(t in this.towns){
	if(GSTown.IsWithinTownInfluence(t.id, GSIndustry.GetLocation(i))){
	result.append(Industry(i, t.weakref()));
	Log.Info("Industry Found", Log.LVL_INFO);	
	}
	}
	}
	}
Industry is a class that holds the industry's I.D. and the town it's in. This means that while when a town grows over an industry I will have to do some extra work I only have to do the nasty O(n^2) stuff once. It holds a reference to the town object so I can access my custom variables iff I want to be able to adjust my industries accordingly. (for example to remove an unused industry in the city center). Other than that it is the same thing. So I start up a temperate map. I see no "industry found". So far so good, by default OTTD doesn't put industries in towns on purpose, just randomly.

But when I start a tropical map, the same happens. I would expect to, at the very least, observe one 'Industry found" per water tower, no?

What is happening here :?:

the_industries() isn't empty. this.towns certainly isn't empty. Besides foreach gives you an error if any of these two is empty. So the if() statement on line 9 [if(GSTown.IsWithinTownInfluence(t.id, GSIndustry.GetLocation(i)))] never returns true. Why?
User avatar
Zuu
OpenTTD Developer
OpenTTD Developer
Posts: 4553
Joined: 09 Jun 2003 18:21
Location: /home/sweden

Re: CB Script

Post by Zuu »

Code: Select all

  local the_industries = GSIndustryList();
   foreach(i in the_industries)
That code will loop over all values in the the_industries list. What you certainly want is to loop over all keys (industry IDs). To do so you can do like this:

Code: Select all

foreach(i, value in the_industries)
However, if you don't use the value, the common way to express that is to use a single underscore as looping variable for the value:

Code: Select all

foreach(i, _ in the_industries)



This is because GSIndustryList and all other variants of GSList key+value par for each item. Though it the API the name for 'key' is 'item'. The idea is that you can use a Valuator to set a value that is associated with each item. Then you can do eg. .KeepAboveValue(5) to only keep the items where the value is > 5.
My OpenTTD contributions (AIs, Game Scripts, patches, OpenTTD Auto Updater, and some sprites)
Junctioneer (a traffic intersection simulator)
Aphid
Traffic Manager
Traffic Manager
Posts: 168
Joined: 16 Dec 2011 17:08

Re: CB Script

Post by Aphid »

Seems everything's working now. No more code questions.
I'll go and test it, should have a working copy of the script soon.

I also added a 5% variation in the goal requirements.
I'm also thinking about a toggleable setting that will randomly jumble n requirements (different for each town)
Would that be actually used? (Certainly not for competitive, but maybe cooperative?)

For example, 140, 160 promille requirement monthly for cargotype 1 and 2 respectively could be jumbled to 70,230, or 120,180, or what have you.
Makes optimum strategy more complex. It's also a terribly hard math problem to figure out what happens if n-> infinity. Not that that's of any use to me, thankfully.

topic title edit.
User avatar
Zuu
OpenTTD Developer
OpenTTD Developer
Posts: 4553
Joined: 09 Jun 2003 18:21
Location: /home/sweden

Re: CB Script

Post by Zuu »

Aphid wrote:Seems everything's working now. No more code questions.
I'll go and test it, should have a working copy of the script soon.
It would be nice if you publish it on Bananas when you feel satisfied that you have a first working version. It doesn't have to be feature complete. Just that all obvious bugs and crashes have been ironed out so that the script is usable for users. But you don't have to wait until it is rock solid and complete, because then it would take forever. If you have something that have known bugs that you don't want to add to bananas, it can be uploaded here to share it still. Or if you want to make your first release on forums only that is fine too. Although you will get more testers via bananas.

Aphid wrote:I also added a 5% variation in the goal requirements.
I'm also thinking about a toggleable setting that will randomly jumble n requirements (different for each town)
Would that be actually used? (Certainly not for competitive, but maybe cooperative?)

For example, 140, 160 promille requirement monthly for cargotype 1 and 2 respectively could be jumbled to 70,230, or 120,180, or what have you.
Makes optimum strategy more complex. It's also a terribly hard math problem to figure out what happens if n-> infinity. Not that that's of any use to me, thankfully.
I think that is a good idea. In 'Neighbours are important', I added a twist by multiplying the goals by a factor that is determined by the size relation between the town in question and it neighbours. (in a such way that you need to grow the neighbours too or the goals will grow very steep)

In that script there is also a time consuming setup, but if you do all setup before calling your first GSController.Sleep(1), then you will get some 2500 ticks or so that execute without delay before the map starts. When Sleep(1) is called, OpenTTD will finish map gen and the game will start. On 1024x1024 or larger with high town count, my GS will not be able to finish setup before the game starts. But on most normal map sizes it should do okay. Just for your knowledge and comparison.
My OpenTTD contributions (AIs, Game Scripts, patches, OpenTTD Auto Updater, and some sprites)
Junctioneer (a traffic intersection simulator)
Aphid
Traffic Manager
Traffic Manager
Posts: 168
Joined: 16 Dec 2011 17:08

Re: CB Script

Post by Aphid »

Something weird:

GSTile.DemolishTile(tile) requires you to be in 'company mode'. It's the only function that can clear a house I found yet. Therefore, I can't bulldoze houses. (there's all sorts of issues with company mode, not the very least of which is that the company could be unable to demolish a house due to not enough money or not enough town authority rating.) If GSCompany were somewhat more powerful (read: able to cheat,create a new company, and set it's town ratings and money) this would not be an issue. I could set up company #1 as a dummy company with $<max(int64)> that would demolish houses.

There seems to be no function or event upon someone clicking 'fund town'. This messes around with both my script and 'neighbours are important', because one can theoretically grow a town infinitely big no matter what we set for requirements, as fund town builds one house effective near-instantly. If I want to change the behaviour to <build near-instantly, except when <condition>> this appears impossible to do reliably. For example, if I checked for the condition daily and set the town's house ticker back to 999999 when it is false, there still is a % chance 'fund buildings' will succeed in what it is doing, as it might be clicked near the end of a day.

It'd be great if GSTown could disable <it's> funding type. E.g. disable fund town functionality in that town.

As it is now, that are the two major gameplay issues, and I can't really do much to fix it. From what I understand, the original implementation at luukland changed the function that builds a house to one that builds a tile of nothing, reversing growth, in some hacky way.
User avatar
Zuu
OpenTTD Developer
OpenTTD Developer
Posts: 4553
Joined: 09 Jun 2003 18:21
Location: /home/sweden

Re: CB Script

Post by Zuu »

I agree that it is probably so that Game Scripts should be allowed to do more actions for free without having to abuse a company. However, this needs to be coded and verified to not break something else. I've started with construction of industries here (screenshot :p), but need to ensure I don't screw up NewGRFs in a bad way.

If you want an example of a work-around, you can take a look at the Split scenario. There the GS have a rich uncle as in an IdleMore AI with plenty of money to fund raising of land out of the sea. For scenarios it should be fairly straight forward to make a workaround like this, but for generic GSs a different solution is needed (see above)
My OpenTTD contributions (AIs, Game Scripts, patches, OpenTTD Auto Updater, and some sprites)
Junctioneer (a traffic intersection simulator)
Post Reply

Return to “OpenTTD AIs and Game Scripts”

Who is online

Users browsing this forum: No registered users and 17 guests