Map Database  •  FAQ  •  RSS  •  Login

Dynamic Script Usage Questions

<<

Siegfried

User avatar

Knight

Posts: 494

Joined: 24 Jul 2009, 22:00

Post 18 May 2013, 12:35

Re: Dynamic Script Usage

OK, I need help.

#1
I want to display some information on the top left with Actions.SetOverlayText. Party to display debug values, partly for a later information display.

I discovered, that a second invoke of Actions.SetOverlayText overrides the first text display. This makes it neccessary to write one single function that displays the whole text. BUT: i don't have a global string available (or a array of strings) to store all the text that is created in different functions throughout the whole code. So how to proceed?

If it's not clear what I want, let me try to explain with some pseudo-code:
  Code:
procedure displayText(txt: string); begin Actions.SetOverlayText(ID, txt); end; procedure crazystuffONE; begin displayText('one'); end; procedure crazystuffTWO; begin displayText('two'); end; procedure OnTick; begin crazystuffONE; crazystuffTWO; end;
This displays only 'two', so I assume that it overwrites the 'one' .

But how do I manage to display bot words, best would be in separate lines?
Btw. the libx-file does not help in this case, because my text is dynamically created.

The basic idea would be to create a global string-array and let the different functions add their lines to that string-array. Then finally, this string-array is displayed once. But global arrays are not available, even global strings are not.


#2
why does your version of PascalScript not support const arrays?
According to the pascalscript documentation I use, something like
  Code:
const a: array[0..2] of integer = (1,2,3);
should be valid, but it gives not with your version.
Even a
  Code:
const a: integer = 1;
does not work.
Your pascalscript behaves on const like C does with #DEFINE - a mere text substitution. Why is this?
<<

Lewin

User avatar

KaM Remake Developer

Posts: 3822

Joined: 16 Sep 2007, 22:00

KaM Skill Level: Skilled

ICQ: 269127056

Website: http://lewin.hodgman.id.au

Yahoo Messenger: lewinlewinhodgman

Location: Australia

Post 18 May 2013, 13:31

Re: Dynamic Script Usage

PascalScript (http://www.remobjects.com/ps) is a compiler/bytecode interpreter library we use to run the dynamic scripts. It might not support every feature of Pascal, constant arrays might be one of those things, I haven't tested it. Maybe the PascalScript compiler does use text substitution like you suggested. Is that documentation you refer to specifically for the PascalScript library (not just Pascal in general?). If so, can you show me and I'll see if I can figure out why it doesn't work.

Global dynamic arrays will be supported in the next RC (static arrays are already supported). Global strings are not supported because it will cause saves files to become inconsistent based on locale (which causes problems in multiplayer). Remember that all global variables are stored and remembered in save files.

We might change the way we handle strings to use a markup for loading text from the LIBX file rather than a function. That way saving strings would be safe because the markup would be the same no matter what locale you are using (and the markup would only be evaluated when the string is displayed). Although that makes Format complicated to fit into the equation...
<<

Siegfried

User avatar

Knight

Posts: 494

Joined: 24 Jul 2009, 22:00

Post 18 May 2013, 13:44

Re: Dynamic Script Usage

I don't use one specific documentation, I usually just google for it. But I always look if it's for PascalScript, not only for Pascal. I think PascalScript should be standardized. At least for those basic things like a simple declaration. I can't imagine that a dialect that changes that. Maybe it's a missing feature from the interpreter engine?

Do you have a solution for the text display problem? If I can't do a global string array, I still don't really see another way to do that.
<<

Lewin

User avatar

KaM Remake Developer

Posts: 3822

Joined: 16 Sep 2007, 22:00

KaM Skill Level: Skilled

ICQ: 269127056

Website: http://lewin.hodgman.id.au

Yahoo Messenger: lewinlewinhodgman

Location: Australia

Post 18 May 2013, 15:15

Re: Dynamic Script Usage

I think PascalScript should be standardized. At least for those basic things like a simple declaration. I can't imagine that a dialect that changes that. Maybe it's a missing feature from the interpreter engine?
PascalScript is open source so you can standardise it if you like ;)
Do you have a solution for the text display problem? If I can't do a global string array, I still don't really see another way to do that.
Not immediately, we didn't initially design the scripts to do things like that with strings (we envisaged simpler uses for dynamic scripts, not realising their full potential). Krom and I have discussed using markup for LIBX strings and agree that it seems like a good solution (so you write '$123' to get string 123 from LIBX and States.GetText is removed). Once we've changed that, there's no reason to restrict storing strings as global variables, so you can implement it like you described.
<<

The Dark Lord

User avatar

King Karolus Servant

Posts: 2154

Joined: 29 Aug 2007, 22:00

KaM Skill Level: Veteran

Location: In his dark thunderstormy castle

Post 18 May 2013, 21:18

Re: Dynamic Script Usage

Okay I've tried many things but I can't get it to work. This is what I want:

SPOILER ALERT FOR UPCOMING TNL 1

On my map, there is a small passage (two tiles broad) where players can sneak through. I want to make the AI attack the player if he tries to sneak through the passage. I wrote something like this:
  Code:
procedure OnTick; begin if (States.GameTime < 7250) then begin SneakingUnit1 := States.UnitAt(57, 52); SneakingUnit2 := States.UnitAt(57, 53); if SneakingUnit1 <> -1 then begin Actions.GroupOrderAttackUnit(States.GroupAt(65, 39), SneakingUnit1); end; if SneakingUnit2 <> -1 then begin Actions.GroupOrderAttackUnit(States.GroupAt(62, 44), SneakingUnit2); end; end; end;
I've tried countless variations to this code but nothing works. And I have one more problem. If the AI loses any soldiers before the attack starts (so when the player is bold enough to attack the AI), the AI should retaliate by doing an all-in attack:
  Code:
if (States.GameTime < 7250) and ((States.StatArmyCount(2) < 60) or (States.StatArmyCount(4) < 60) or (States.StatArmyCount(5) < 24)) then begin ??? end;
Currently, there is nothing to place on the ???. I don't want to make the AI attack the player's closest unit or closest building... I want the AI to attack whatever is closest to him and keep attacking until everything is dead or destroyed. Also, I find "GroupOrderAttackUnit" to be extremely frustrating, as it does not allow to attack a group but only a unit... It's not easy to combine this command with "GroupAt", for example.
<<

sado1

User avatar

Council Member

Posts: 1430

Joined: 21 May 2012, 19:13

KaM Skill Level: Skilled

Post 18 May 2013, 23:51

Re: Dynamic Script Usage

The longer code seems alright, in fact my script has the exact same code (except variable names) and it's working... no idea why it doesn't work.

About all-in attacks:
  Code:
Units := PlayerGetAllGroups(PlayerID); for I := 0 to (Length(Units)-1) do GroupOrderAttackUnit(I, the_poor_attacked_unit);
Also, you can use GroupOwner to extract the group main unit (flag bearer), and tell GroupOrderAttackUnit to attack that unit. Which makes you attack group with group.
<<

Lewin

User avatar

KaM Remake Developer

Posts: 3822

Joined: 16 Sep 2007, 22:00

KaM Skill Level: Skilled

ICQ: 269127056

Website: http://lewin.hodgman.id.au

Yahoo Messenger: lewinlewinhodgman

Location: Australia

Post 19 May 2013, 03:16

Re: Dynamic Script Usage

I've tried countless variations to this code but nothing works.
The code looks ok to me. I suggest you try debugging: Make it display a message when it detects a unit standing in the passage, then you can see whether it's the detecting or the ordering to attack that is the problem. Are you aware that the coordinate system used in the game (and .script files) is different to the one used in .dat? In the game/.script the top left tile of the map is 1;1, but in the .dat scripts it's 0;0. So maybe you've used the wrong coordinates? Also, check the latest log file, it might contain errors that were caused by the script.
Currently, there is nothing to place on the ???. I don't want to make the AI attack the player's closest unit or closest building... I want the AI to attack whatever is closest to him and keep attacking until everything is dead or destroyed.
In that case I'd have a global Boolean variable DoMassiveAttack which you set to true when you want the AI to attack. Then in OnTick you can check if DoMassiveAttack is true, and if it is, order the AI to attack the player every 10 seconds or so (no need to do it every tick).

As Sado said you can write a function which makes all the AIs groups attack. You could also write a function to give you the closest unit (or house) of the human player (using PlayerGetAllGroups), although I'm considering providing GetClosestUnit/Group/House functions because it's a pretty common thing to want.

If you want help with the code for any of this, let me know and I'll write some examples here.
Also, I find "GroupOrderAttackUnit" to be extremely frustrating, as it does not allow to attack a group but only a unit... It's not easy to combine this command with "GroupAt", for example.
When you order a group to attack a unit they will target that specific unit, but once he's dead they will then chase down his group too (same as it now works for chasing down a group of fleeing archers). So functionally it's basically the same as attacking a group, but it tells them one unit to target first.

Instead of "GroupAt" you could use "UnitAt". But if you want to order them to attack a group, then you can do:
Actions.GroupOrderAttackUnit(MyGroup, States.GroupMember(TargetGroup, 0));
That orders them to attack the flag bearer (member 0) of the target group (which you could replace with GroupAt if you like)

@sado: There's a mistake in your code, you used "I" instead of "Units". It should be:
GroupOrderAttackUnit(Units, the_poor_attacked_unit);
<<

Siegfried

User avatar

Knight

Posts: 494

Joined: 24 Jul 2009, 22:00

Post 19 May 2013, 12:00

Re: Dynamic Script Usage

I think PascalScript should be standardized. At least for those basic things like a simple declaration. I can't imagine that a dialect that changes that. Maybe it's a missing feature from the interpreter engine?
PascalScript is open source so you can standardise it if you like ;)
Haha, that came out totally wrong :) I thought PascalScript already were standardized, so I cannot imagine problems occuring because of a different syntax.

But now I see another problem that is not documented well.

I keep an array with the players by fetching them from States.HouseOwner(storehouse at start). In a 8 player map the numbers are from 0 ..7.

Now I want to set the victory condition.
Actions.PlayerWin now needs the ominous 'player index' that is not further described in the wiki page. Is the number needed here the same that I get from HouseOwner?

I wonder about the connection to the locations. Let's say I start on loc 2 and have two players at loc 5 and loc 8. The results I get from HouseOwner then are 1, 4 and 7.
So is that 'player index' connected to the locations? Isn't there an array with 0, 1, 2 that describes the three players present on that map?

I just ask because sometimes it's easier to iterate over 0,1,2 than to iterate the full 8 players and checking which ones are present.
<<

Lewin

User avatar

KaM Remake Developer

Posts: 3822

Joined: 16 Sep 2007, 22:00

KaM Skill Level: Skilled

ICQ: 269127056

Website: http://lewin.hodgman.id.au

Yahoo Messenger: lewinlewinhodgman

Location: Australia

Post 19 May 2013, 13:06

Re: Dynamic Script Usage

HouseOwner returns a player index (0..7) same as PlayerWin expects. Player index 0 = location 1 on the map, index 7 = location 8. So if you have a multiplayer map with 8 locations but only 3 active players, they won't necessarily be index 0..2, since they might not have selected locations 1..3 in the lobby. PlayerEnabled will tell you if that player index is used.
<<

The Dark Lord

User avatar

King Karolus Servant

Posts: 2154

Joined: 29 Aug 2007, 22:00

KaM Skill Level: Veteran

Location: In his dark thunderstormy castle

Post 19 May 2013, 14:59

Re: Dynamic Script Usage

I've tried countless variations to this code but nothing works.
The code looks ok to me. I suggest you try debugging: Make it display a message when it detects a unit standing in the passage, then you can see whether it's the detecting or the ordering to attack that is the problem. Are you aware that the coordinate system used in the game (and .script files) is different to the one used in .dat? In the game/.script the top left tile of the map is 1;1, but in the .dat scripts it's 0;0. So maybe you've used the wrong coordinates? Also, check the latest log file, it might contain errors that were caused by the script.
Okay I got it working now. I didn't realise GroupAt was influenced by this as well. And I was a bit unlucky with the detecting, as I tested all tiles around it except for the one that should have triggered the attack (which was a mountain tile due to these wrong coordinates). I thought it couldn't go wrong because it was a 2 tile broad entrance, so a 1 tile difference shouldn't have mattered. However, to keep things simple I disabled part of the script so I could only test 1 tile at a time, and I chose the wrong tile... -_-

Anyway! The attack works now, using this code:
  Code:
if (States.GameTime < 7250) then begin SneakingUnit1 := States.UnitAt(58, 53); SneakingUnit2 := States.UnitAt(58, 54); if SneakingUnit1 <> -1 then begin Actions.GroupOrderAttackUnit(Scout3, SneakingUnit1); end; if SneakingUnit2 <> -1 then begin Actions.GroupOrderAttackUnit(Scout2, SneakingUnit2); end; end;
However, I'm noticing several things:
- While I use only one unit to step on tile (58, 54), both Scout2 and Scout3 are triggered.
- After killing my unit, Scout2 and Scout3 rush back to (58, 53) and (58, 54) and keep 'attacking' that location; even though there just isn't anything to attack

I figured the AI was trying to attack his own units because he stepped on those tiles as well. I expanded my code:
  Code:
if (States.GameTime < 7250) then begin SneakingUnit1 := States.UnitAt(58, 53); SneakingUnit2 := States.UnitAt(58, 54); if (SneakingUnit1 <> -1) and (States.UnitOwner(SneakingUnit1) = 0) then begin Actions.GroupOrderAttackUnit(Scout3, SneakingUnit1); end; if (States.UnitDead(SneakingUnit1) = True) and (States.GroupMemberCount(Scout3) = 8) then begin Actions.GroupOrderWalk(Scout3, 66, 40, 6); end; if (SneakingUnit2 <> -1) and (States.UnitOwner(SneakingUnit2) = 0) then begin Actions.GroupOrderAttackUnit(Scout2, SneakingUnit2); end; if (States.UnitDead(SneakingUnit2) = True) and (States.GroupMemberCount(Scout2) = 8) then begin Actions.GroupOrderWalk(Scout2, 63, 45, 6); end; end;
As you can see I also tried to make the AI retreat once the unit was killed. However, I noticed a bug. Ignoring the fact that this script doesn't work as I want it to (the AI retreats immediately when you leave the 'trigger tiles', so apparently it already assumes SneakingUnit is dead), the pathfinding seems bugged. This is what happens:
1. I step on tile (58, 53)
2. Scout3 is triggered and attacks me
3. I step on tile (57, 53)
4. Scout3 retreats immediately
5. Now I step on tile (58, 54)
6. Scout2 TRIES to attack me but is somehow unable to walk past Scout 3. Scout 2 seems to be trying to go through Scout 3, but Scout3 doesn't move an inch.
Also, both Scout 2 and Scout 3 do not face the correct direction (west) when they retreat.

I'll now try to script the fearsome all-in attack. :P And maybe if I come up with something I'll attempt to make the AI retreat after he actually killed SneakingUnit. Thanks for your answer (and sado too, guess I should've listened to you after all ;) ).
In that case I'd have a global Boolean variable DoMassiveAttack which you set to true when you want the AI to attack. Then in OnTick you can check if DoMassiveAttack is true, and if it is, order the AI to attack the player every 10 seconds or so (no need to do it every tick).

As Sado said you can write a function which makes all the AIs groups attack. You could also write a function to give you the closest unit (or house) of the human player (using PlayerGetAllGroups), although I'm considering providing GetClosestUnit/Group/House functions because it's a pretty common thing to want.
Yeah, the problem is the target. I do not want to make the AI attack units only, or houses only. They have to attack everything. Surely I could write a script where the AI attacks all units first and if unit count = 0 then they start destroying houses. But this means they will chase one pathetic newly trained soldier to the border of the map before they continue destroying houses, which will just look silly...
<<

Lewin

User avatar

KaM Remake Developer

Posts: 3822

Joined: 16 Sep 2007, 22:00

KaM Skill Level: Skilled

ICQ: 269127056

Website: http://lewin.hodgman.id.au

Yahoo Messenger: lewinlewinhodgman

Location: Australia

Post 19 May 2013, 15:34

Re: Dynamic Script Usage

States.UnitDead will return true if the UnitID is -1 (it will also log an error). Basically you should never call States.UnitDead(SneakingUnit1) without first checking "SneakingUnit1 <> -1" (unless you are already sure that it exists). So you should change those lines to:
if (SneakingUnit1 <> -1) and (States.UnitDead(SneakingUnit1) = True) and (States.GroupMemberCount(Scout3) = 8) then

You can code it so the AI will attack which ever is closer, a unit or a house. You could also make it random if you like, so half the AI's troops do one thing, the other half does another thing. Very rough pseudo code:
  Code:
function Distance(X1, Y1, X2, Y2: Integer): Integer; begin Result := Sqrt(Sqr(X1-X2) + Sqr(Y1-Y2)); end; ClosestUnit := -1; ClosestUnitDistance := 999; for each unit of the human player do if Distance(unitX, unitY, 11, 22) < ClosestUnitDistance then begin ClosestUnitDistance := Distance(unitX, unitY, 11, 22); ClosestUnit := unit; end; ClosestHouse := -1; ClosestHouseDistance := 999; for each house of the human player do if Distance(houseX, houseY, 11, 22) < ClosestHouseDistance then begin ClosestHouseDistance := Distance(houseX, houseY, 11, 22); ClosestHouse := house; end; if ClosestUnitDistance < ClosestHouseDistance then for each AI group do AttackUnit(ClosestUnit) else for each AI group do AttackUnit(ClosestHouse);
Maybe that will give you some ideas.
<<

Siegfried

User avatar

Knight

Posts: 494

Joined: 24 Jul 2009, 22:00

Post 20 May 2013, 20:08

Re: Dynamic Script Usage

Is there a way to determine the player id of the active player in MP? As far as I've understood, it's always 0 in singleplayer, but in multiplayer it depends on the location. But how to tell the script which location the current player has?
<<

sado1

User avatar

Council Member

Posts: 1430

Joined: 21 May 2012, 19:13

KaM Skill Level: Skilled

Post 20 May 2013, 21:21

Re: Dynamic Script Usage

Is there a way to determine the player id of the active player in MP? As far as I've understood, it's always 0 in singleplayer, but in multiplayer it depends on the location. But how to tell the script which location the current player has?
What do you mean by 'active/current player'? Not AI? You could check the nickname if that's the case, I guess...
<<

Lewin

User avatar

KaM Remake Developer

Posts: 3822

Joined: 16 Sep 2007, 22:00

KaM Skill Level: Skilled

ICQ: 269127056

Website: http://lewin.hodgman.id.au

Yahoo Messenger: lewinlewinhodgman

Location: Australia

Post 20 May 2013, 22:36

Re: Dynamic Script Usage

Is there a way to determine the player id of the active player in MP? As far as I've understood, it's always 0 in singleplayer, but in multiplayer it depends on the location. But how to tell the script which location the current player has?
What do you mean by 'active/current player'? Not AI? You could check the nickname if that's the case, I guess...
You are not able to access that because it goes against the design of dynamic scripts. Dynamic scripts must executed exactly the same on every client, otherwise out of sync bugs will occur. For example if I do this:
if CurrentPlayer = 4 then Actions.GiveWares(4, ..)
The command "GiveWares" would only be executed only player 4's client. This would cause him to think he has more wares than other players do, and result in an our of sync error. The correct code would be:
Actions.GiveWares(4, ..)
Which means every client would have the same record for how many wares player 4 has, and no out of sync error occurs.

There shouldn't be any cases where you need to know the current player. For example ShowMsg and SetOverlayText will simply be ignored by the client if they are not for their player index (first parameter is player index). But all game logic stuff MUST be executed the same on every client or it goes out of sync.

Can you give an example of how you wanted to use it?
<<

Siegfried

User avatar

Knight

Posts: 494

Joined: 24 Jul 2009, 22:00

Post 21 May 2013, 08:24

Re: Dynamic Script Usage

There shouldn't be any cases where you need to know the current player. For example ShowMsg and SetOverlayText will simply be ignored by the client if they are not for their player index (first parameter is player index). But all game logic stuff MUST be executed the same on every client or it goes out of sync.

Can you give an example of how you wanted to use it?
Well, my example is the following:
i want to display some statistics on the screen. Because of the script limitations, this has to be done in a dynamical way. This dynamic creation of that string is very CPU-demanding, because it calls some heavy API functions.

So my code is the following:
  Code:
procedure ShowStats; begin for i := 0 to 7 do begin DisplayText := HighCPUloadTextCreation( i ); Actions.SetOverlayText( i, DisplayText ); end; end;
So in my case it does not help that SetOverlayText skips the display for 7 players and only shows it for the 8th.
HighCPUloadTextCreation is called 8 times, and then SetOverlayText discards 7 out of these 8.

So I wanted to save a factor of 8 by doing that:
  Code:
procedure ShowStats; begin for i := 0 to 7 do if States.CurrentPlayer then begin DisplayText := HighCPUloadTextCreation( i ); Actions.SetOverlayText( i, DisplayText ); end; end;
This does not alter the savegame structure (at least I hope that you don't save dynamic display text) nor the game state, so no desync will appear. If you set CurrentPlayer := -1 for watching a replay, no problems should occur. At least that's my guess, because I don't really know the details of your program execution structure.

Return to “Dynamic Scripting”

Who is online

Users browsing this forum: Google [Bot] and 4 guests