Staredit Network > Forums > Modding Discussion > Topic: Defining AI commands and functions
Defining AI commands and functions
Jun 21 2017, 8:49 pm
By: Pr0nogo
Pages: 1 2 35 >
 

Jun 21 2017, 8:49 pm Pr0nogo Post #1



UPDATE: Nekron's documentation for AI functions

My latest project has custom AI scripts, and I was able to develop the more complex ones with the assistance of Nekron. He's been working with the default campaign AIs in the three brood war campaigns in an attempt to make them significantly more challenging than before. Both of us will likely be working with AI scripts for a long time, so we're interested in exploring more of the untapped, undefined, and otherwise uncharted functions and opportunities that SC's AI has to offer in a UMS/campaign setting. The goal here is to develop a consistent knowledge basis for current and future AI modders to use when coding scripts, to know what functions are and aren't possible, and to know the limits of possible functions.

The two of us have compiled a 'wishlist' of sorts, including the functions that we currently know little or nothing about. Here's hoping that our reverse-engineers and programmers can decipher the assembly. If something you see here has already been defined, please post the source!

Unknown aspects and functions of the AI

Undefined or poorly-defined AI functions


Post has been edited 1 time(s), last time on Aug 23 2017, 11:55 pm by Pr0nogo.




Jun 22 2017, 8:34 pm Neiv Post #2



Hey, that's something I know a bit about. I wanted to get a more complete grasp on how the AI works and document all of that, but I never got around it, so here's what I know. Most notably I never looked into the attacking logic itself, just how AI base management / unit building works.

= Towns and `build` opcode =
AI's bases are called towns. When the AI expands (`expand` opcode), it creates a new town. I don't know what's the difference between "town" and an "area town". Each of the AI script's threads is assigned to some town, but most of the opcodes modify AI's global state.

Most notably, the `build` opcode modifies the current thread's town's buildings / workers. Using `build` is a permament change to the town's buildings, that is, `build 3 barracks 100` tells the AI that the town should have 3 Barrackses, and the AI will keep rebuilding those 3 barrackses even if they get destroyed (I believe that the zerg morphing buildings have special handling so the AI won't build a Hatchery after it "loses" one by upgrading to Lair). Workers are also part of town's "buildings", but there's special handling that makes AI limit worker building when it thinks there are enough of them.

= Base locations =
At start of the game, AI will scan the map's resources and determine the places that a base can be placed on. If the resources are placed in a unusual way, AI might place its main building to a nonsensical location when expanding. Most notably, all Start Locations of the map will always become base locations, so they should be placed in places which actually are sensible places to expand.

= Town defense =
The `defensebuild_xx` opcodes and variants define AI's defense composition. For example, using `defensebuild_gg 1 marine` and `defensebuild_gg 2 vulture`, will make the AI build 33% marines and 66% vultures when it needs to get units to defend its town.

The `defenseuse_xx` opcodes will allow AI to take military it has built beforehand and use it to defend against attacks. Again, the number will define the ratio of ideal composition of what the defense forces should consist of. The AI will use all of it's preexisting military it has (as long as the unit type is allowed to defend with `defenseuse_xx`), before it starts training units specified with `defensebuild_xx`.

Enemies are separated to four different classes in the town defense opcodes.
Code
`_ga` Enemy ground units attack own air units
`_ag` Enemy air, own ground
`_gg` Enemy ground, own ground
`_aa` Enemy air, own air


Also, there's limit of 20 units per defense_xx in the script, anything after that will be ignored. So "defenseuse_gg marine 10" followed by "defenseuse_gg vulture 9" will leave only one defenseuse_gg slot free. I believe `defenseclear_xx` clears these, but I'm not sure what's the exact behaviour (Does it affect both "use" and "build" ?)

= Unit strength =

When attacked, the AI will bring/train enough, but not too many defenders to defend its town. It determines this ideal number by calculating a "strength value" for each unit. The formula is as follows:
Code
   Strength = sqrt((damage * factor * (range_pixels + 8 * (hitpoints + shields))) / cooldown) * 7.58
     - Workers get 25% of default
     - Interceptors, scarabs, mines 0%
     - Firebats, mutalisks, zealots 200%
     - Defilers, inf. terrans 1/16
     - Reavers 10%


This means that if the units have some cool and flashy iscript attacks which shoot 10 missiles per attack cycle, the AI will misevaluate the units' strength to be far lower than the actual efficency.

So fancy iscript can lead to an AI which over/underreacts when its town gets attacked.

= Training military =
The main training commands are `train`, `wait_train`, `do_morph`, and `wait_force` AI can only train one unit at once (Not counting defense training, which is really common), so multiple threads should not be training units simultaneously.
- `train` will set the unit to be trained, and wait until enough units have at least been started to train. Ai does not understand morphing Zerg units as something that has started to train, so when used with Zerg, it will wait until all units have completed. But `train` should still work fine with Zerg units.
- `wait_force` is same as `train`, but it always waits until the units have been completed.
- `do_morph` will not wait at all, it just sets the unit to be trained. Again, there is nothing Zerg-specific in there. Because the AI can only train one unit at once, using multiple `do_morph`s in succession without any waits is always the incorrect thing to do.
- `wait_train` does not set the unit to be trained, so it will hang the script if other training command didn't set the training unit to what is being waited. Blizzard scripts only use wait_train in short child threads, which call `wait_train` and start an upgrade once the AI has enough units the upgrade is affecting.

= Bugs =
- If AI tries to tech/upgrade something it does not have the necessary tech for, it will start behaving very inefficiently
- If an SCV building a Terran building is killed without destroying the building, AI will try to "repair" the building instead of "continue construction", which will completely break the town's management, as SCVs just stop doing anything.

= Some specialized opcodes =
- `harass_location` is unused and doesn't do anything
- `killable` ends the current thread once any thread calls `kill_thread` (Should use `end` if the thread should just end right now)
- `fake_nuke`. AI has a cooldown on nukes, `fake_nuke` will set the cooldown as if the AI just nuked.
- `help_iftrouble` will make AI treat any current allied computer base areas as places that should be defended.
- `allies_watch`. The first argument is a index of the base location (I don't think there are any tools to determine the index), and second is a script position. If the base location has resources remaining, *something* happens.

Post has been edited 3 time(s), last time on Jul 20 2017, 11:21 pm by Neiv.



None.

Jun 22 2017, 9:35 pm Voyager7456 Post #3

Responsible for my own happiness? I can't even be responsible for my own breakfast

This is great stuff - thanks for the info, Neiv!



all i am is a contrary canary
but i'm crazy for you
i watched you cradling a tissue box
sneezing and sniffling, you were still a fox


Modding Resources: The Necromodicon [WIP] | Mod Night
My Projects: SCFC | ARAI | Excision [WIP] | SCFC2 [BETA] | Robots vs. Humans | Leviathan Wakes [BETA]


Jun 22 2017, 11:44 pm Pr0nogo Post #4



Really appreciate the writeup Neiv. Thanks for the information. I'll see about putting it to use in my scripts.

How would one go about deciphering the attack logic?




Jun 23 2017, 4:11 am Voyager7456 Post #5

Responsible for my own happiness? I can't even be responsible for my own breakfast

On a somewhat related note - are you familiar with whatever AI nonsense is going on in sub_462EA0, Neiv? I'm trying to understand the "return to original position" behavior that AI units display, but I don't think anyone has documented this function. I figured I'd check with you first.



all i am is a contrary canary
but i'm crazy for you
i watched you cradling a tissue box
sneezing and sniffling, you were still a fox


Modding Resources: The Necromodicon [WIP] | Mod Night
My Projects: SCFC | ARAI | Excision [WIP] | SCFC2 [BETA] | Robots vs. Humans | Leviathan Wakes [BETA]


Jun 23 2017, 5:18 am Nekron Post #6



Quote
AI's bases are called towns. When the AI expands (`expand` opcode), it creates a new town. I don't know what's the difference between "town" and an "area town". Each of the AI script's threads is assigned to some town, but most of the opcodes modify AI's global state.

Most notably, the `build` opcode modifies the current thread's town's buildings / workers. Using `build` is a permament change to the town's buildings, that is, `build 3 barracks 100` tells the AI that the town should have 3 Barrackses, and the AI will keep rebuilding those 3 barrackses even if they get destroyed (I believe that the zerg morphing buildings have special handling so the AI won't build a Hatchery after it "loses" one by upgrading to Lair). Workers are also part of town's "buildings", but there's special handling that makes AI limit worker building when it thinks there are enough of them.

Areatowns check for buildings only in the vicinity of their location in Staredit. You're right about Zerg buildings. Workers have a upper limit of 25 per town, they usually get built with build commands and aren't unlimited anyway in campaign scripts though.

Quote
The `defensebuild_xx` opcodes and variants define AI's defense composition. For example, using `defensebuild_gg 1 marine` and `defensebuild_gg 2 vulture`, will make the AI build 33% marines and 66% vultures when it needs to get units to defend its town.

You're sure that it affects the ratio per se? In Blizzard's BW T8B there's "defensebuild_gg 2 infested_terran
defenseuse_gg 2 infested_terran", which makes them always build IT's in pairs for defense. I would think that it just states how many units should be trained at once. This also affects default_min (number) where (number) is greater than 0 and makes it send for example 2 or 3 units to expansions if the defenseuse number is 2 or 3. I mean, it really is the ratio, but I'm not sure whether it will be showcased if you only have one factory and only 1 barracks in your example, for uhhh... example?
Quote
= Unit strength =

Just to clarify - these values are what's used for max_force and harass_factor (in harrass_factor's case, AI would calculate it for the enemy player I figure?), right? Or just max_force?
(Also, for the record - do Infested Terrans have an attack cooldown?)

Quote
- If an SCV building a Terran building is killed without destroying the building, AI will try to "repair" the building instead of "continue construction", which will completely break the town's management, as SCVs just stop doing anything.

It breaks the town management in the same way as currently building Zerg buildings do in a Terran area town, and it gets resolved once the building is destroyed, so I think it's not too big of a deal usually (Edit: Not in campaign. I understand that it can cause lots of problems to an skirmish bot)
Quote
- `train` will set the unit to be trained, and wait until enough units have at least been started to train. Ai does not understand morphing Zerg units as something that has started to train, so when used with Zerg, it will wait until all units have completed. But `train` should still work fine with Zerg units.

It tends to break the AI by making it build unlimited amounts of hydras or other units used by default_build I find, even when it's turned off. Works perfectly fine on Queens and Defilers, doesn't work at all on Infested Terrans I think. Or at least I personally could not get it to work. It's the same with other train-related commands

Quote
- `allies_watch`. The first argument is a index of the base location (I don't think there are any tools to determine the index), and second is a script position. If the base location has resources remaining, *something* happens.
The 1-8 values in index of allies_watch refer to the start locations. It's unfortunate that higher numbers can't be determined :/

Also, wouldn't you happen to know what value %2 in if_dif (%1) (%2) (block) refers to? I would like to use it to coordinate scripts without using map triggers (because I assume that the value is global, correct me if I'm wrong), but it's kinda hard to do when I have no idea what number to put in here.

Post has been edited 2 time(s), last time on Jun 23 2017, 5:24 am by Nekron.




Jun 23 2017, 5:34 am Voyager7456 Post #7

Responsible for my own happiness? I can't even be responsible for my own breakfast

Quote from Nekron
(Also, for the record - do Infested Terrans have an attack cooldown?)

Infested Terrans have an attack cooldown of 1.



all i am is a contrary canary
but i'm crazy for you
i watched you cradling a tissue box
sneezing and sniffling, you were still a fox


Modding Resources: The Necromodicon [WIP] | Mod Night
My Projects: SCFC | ARAI | Excision [WIP] | SCFC2 [BETA] | Robots vs. Humans | Leviathan Wakes [BETA]


Jun 23 2017, 3:50 pm Neiv Post #8



Quote from Nekron
You're sure that it affects the ratio per se? In Blizzard's BW T8B there's "defensebuild_gg 2 infested_terran
defenseuse_gg 2 infested_terran", which makes them always build IT's in pairs for defense. I would think that it just states how many units should be trained at once. This also affects default_min (number) where (number) is greater than 0 and makes it send for example 2 or 3 units to expansions if the defenseuse number is 2 or 3. I mean, it really is the ratio, but I'm not sure whether it will be showcased if you only have one factory and only 1 barracks in your example, for uhhh... example?
Right, I'd assume that the AI is rarely actually able to fulfill the specified ratio, and it'll just end up training units from anywhere it can to get strong enough force to defend. But if the units are trained from same building, say 1 Marine-2 Firebat -ratio, then it *should* mostly end up being 66% Firebats.

Oh, I also realized that the defense limit is 20 units total, e.g. "defensebuild_gg 20 marine" would use all of the defensebuild_gg slots.

Quote
Just to clarify - these values are what's used for max_force and harass_factor (in harrass_factor's case, AI would calculate it for the enemy player I figure?), right? Or just max_force?
I cannot say right now which values those opcodes use, but the AI does calculate both enemy and own strengths for various things.

Quote
Quote
- `train` will set the unit to be trained, and wait until enough units have at least been started to train. Ai does not understand morphing Zerg units as something that has started to train, so when used with Zerg, it will wait until all units have completed. But `train` should still work fine with Zerg units.

It tends to break the AI by making it build unlimited amounts of hydras or other units used by default_build I find, even when it's turned off. Works perfectly fine on Queens and Defilers, doesn't work at all on Infested Terrans I think. Or at least I personally could not get it to work. It's the same with other train-related commands
Maybe the AI doesn't get that it is already making Hydras as it can't see inside eggs so it builds more than necessary? Not sure why Queens and Defilers work. But I also thought that if the AI isn't doing anything else,it should just keep training the latest unit it was ordered to, even if it's not necessary (Terran and Protoss AIs too).

Quote
The 1-8 values in index of allies_watch refer to the start locations. It's unfortunate that higher numbers can't be determined :/
Somebody could write a GPTP plugin to show them though. The ids stay same as long as map's terrain/preplaced units don't change.

Addresses / structures if someone wants to write such plugin


Quote from Voyager7456
On a somewhat related note - are you familiar with whatever AI nonsense is going on in sub_462EA0, Neiv? I'm trying to understand the "return to original position" behavior that AI units display, but I don't think anyone has documented this function. I figured I'd check with you first.
It checks if a "guard" should return back to its original position. The code seems to just check first that the unit is a guard, and then does additional checks (Is the unit in combat, how far away it has moved, etc), and either leaves the unit alone or orders it back to the original position.

Guard ai structure (Pointer at unit + 0x134)

Preplaced units and the units specified by place_guard opcodes become guards, other AI-trained units don't.
The AI will also try to replace guards that have been lost if it gets chance, so the GuardAi structure persists after the guard has died/removed/whatever. That causes AI-controlled units in some UMS maps to freeze after all of the 1000 GuardAi structures have been used.



None.

Jun 23 2017, 5:07 pm Nekron Post #9



Quote from Neiv
Preplaced units and the units specified by place_guard opcodes become guards, other AI-trained units don't.
The AI will also try to replace guards that have been lost if it gets chance, so the GuardAi structure persists after the guard has died/removed/whatever. That causes AI-controlled units in some UMS maps to freeze after all of the 1000 GuardAi structures have been used.
That's great to know, I did notice that preplaced units were often replaced, but didn't know what they're literally the same thing as place_guard.

Still nothing on if_dif though, FeelsBadMan. My dreams of script coordination lie in shambles ;-;

Quote from Neiv
Maybe the AI doesn't get that it is already making Hydras as it can't see inside eggs so it builds more than necessary? Not sure why Queens and Defilers work. But I also thought that if the AI isn't doing anything else,it should just keep training the latest unit it was ordered to, even if it's not necessary (Terran and Protoss AIs too).

I just now got training Zerg units (ultras specifically) to work in BW Z3A, the issue was probably using a biracial script previously (BW Z8A/D). I'm not sure if training mutas/hydras work, honestly I had really bad experiences with zerg default_build units xD They always somehow leak through and ignore define_max limits, even when default_build is disabled, especially with capt_expand called with default_min greater than 0 and devourers or guardians defined as the 1st defensebuild_aa unit.




Jun 23 2017, 5:45 pm Voyager7456 Post #10

Responsible for my own happiness? I can't even be responsible for my own breakfast

Quote from Neiv
Quote from Voyager7456
On a somewhat related note - are you familiar with whatever AI nonsense is going on in sub_462EA0, Neiv? I'm trying to understand the "return to original position" behavior that AI units display, but I don't think anyone has documented this function. I figured I'd check with you first.
It checks if a "guard" should return back to its original position. The code seems to just check first that the unit is a guard, and then does additional checks (Is the unit in combat, how far away it has moved, etc), and either leaves the unit alone or orders it back to the original position.

Guard ai structure (Pointer at unit + 0x134)

Preplaced units and the units specified by place_guard opcodes become guards, other AI-trained units don't.
The AI will also try to replace guards that have been lost if it gets chance, so the GuardAi structure persists after the guard has died/removed/whatever. That causes AI-controlled units in some UMS maps to freeze after all of the 1000 GuardAi structures have been used.

Thank you, Neiv. Your explanation of the GuardAi structure was incredibly useful.



all i am is a contrary canary
but i'm crazy for you
i watched you cradling a tissue box
sneezing and sniffling, you were still a fox


Modding Resources: The Necromodicon [WIP] | Mod Night
My Projects: SCFC | ARAI | Excision [WIP] | SCFC2 [BETA] | Robots vs. Humans | Leviathan Wakes [BETA]


Jun 23 2017, 6:10 pm Neiv Post #11



It seems that there was some unused concept of "AI difficulty". `if_dif` would jump if the the difficulty was less (param 1 is 0)/greater (param 1 is not 0) than second parameter, but the difficulty now is always 1, unless the AI has never started a town, in which case it is 0. There is some unused functionality which would allow AI to mine more than 8 resources per trip if the difficulty was ever 2.

Also, `easy_attack` functions the same as `attack_add`, but only if this "diffiulty" value is 0. Otherwise it doesn't do anything.

`prep_down <a> <b> <unit>` is also similar as attack_add, but it can be used to add more units if the AI commands more of them: It is practically `attack_add max(unit_count(unit) - a, b) unit`

`clear_combatdata` requires a location and then it clears all region analysis from that area. The AI keeps analysis of the map's regions, whether those regions are AI's town, attack target, area which cannot be defended, etc. Not too sure of the meanings of this region analysis, and how quickly the AI will reanalyse whatever was cleared.

Edit: `value_area` makes the AI treat regions of the location same as its town area, defending them.

Post has been edited 1 time(s), last time on Jun 23 2017, 6:32 pm by Neiv.



None.

Jun 23 2017, 6:28 pm Nekron Post #12



Holy damn that's a really interesting bit of information with if_dif. Makes it impossible to use how I wanted to use it, so I'll just need to stick with random_jump's, but the fact that there was difficulty planned is really interesting.

Thanks for your hard work Neiv! :D Now there's almost no undefined commands left, I think only harass_factor (Still no idea how to position it in a script, tried a couple of ways and AI hung up :P) and wait_secure out of these are hard to test manually, and maybe eval_harass with the impossible to test values.




Jun 24 2017, 4:35 pm Nekron Post #13



So I tried to use prep_down in a script and I honestly can't get it to work - I'm not sure if it's because of something else, but I tried taking away all the problems that could be problematic (whole defensive_matrix block, only the wait_force part, send_suicide, excess supply) and it still stopped attacking after a wave or two. If you Neiv (or anyone else) have any idea why it doesn't work I'd be grateful for any tips on how to fix it. For reference, it's supposed to be used in BW P7A.
https://pastebin.com/raw/0GiSRvq6




Jun 25 2017, 1:35 am Pr0nogo Post #14



After some theorycrafting with Nekron I'd like to open up the request to our resident coders to look into potentially programming a function for casting more spells. Currently we only have the nuke, disruption web, and recall spells castable by the AI using a script and a location. The idea would be similar to creating new iscript opcodes that would be scriptable in PyAI/SCAIedit, and having them issue an order to a unit.

The easiest spells I can imagine would be spells that can be cast on terrain - EMP Shockwave, Ensnare, etc. However, seeing as how all abilities can be modified to be cast on terrain, ideally scripts would be made for all spells. I would imagine that the codebase structure for this can be found with the disruption web function. Hopefully the script is as simple as issuing an order with the location as the target.

For spells that do require targets, I imagine that a function could be coded to pick a unit in a location and set that as the target for the order.

Can any of our coders appraise this for its complexity/plausibility?




Jun 25 2017, 2:30 pm poiuy_qwert Post #15

PyMS and ProTRG developer

Quote from Pr0nogo
The idea would be similar to creating new iscript opcodes that would be scriptable in PyAI/SCAIedit, and having them issue an order to a unit.
Easy for me to add commands to PyAI, the issue is the plugin that is needed to drive the new AI commands.

Quote from Pr0nogo
Can any of our coders appraise this for its complexity/plausibility?
The plugin is probably not that hard to make, the hard part is getting someone to do it :P




Jun 28 2017, 11:59 am Neiv Post #16



Quote from Nekron
So I tried to use prep_down in a script and I honestly can't get it to work - I'm not sure if it's because of something else, but I tried taking away all the problems that could be problematic (whole defensive_matrix block, only the wait_force part, send_suicide, excess supply) and it still stopped attacking after a wave or two. If you Neiv (or anyone else) have any idea why it doesn't work I'd be grateful for any tips on how to fix it. For reference, it's supposed to be used in BW P7A.
https://pastebin.com/raw/0GiSRvq6

The script seemed to work perfectly fine for me o.o

If you can share a replay that shows the issue and the .mpq that was used, I could try figuring it out from there.



None.

Jun 28 2017, 1:54 pm Nekron Post #17



Quote from Neiv
Quote from Nekron
So I tried to use prep_down in a script and I honestly can't get it to work - I'm not sure if it's because of something else, but I tried taking away all the problems that could be problematic (whole defensive_matrix block, only the wait_force part, send_suicide, excess supply) and it still stopped attacking after a wave or two. If you Neiv (or anyone else) have any idea why it doesn't work I'd be grateful for any tips on how to fix it. For reference, it's supposed to be used in BW P7A.
https://pastebin.com/raw/0GiSRvq6

The script seemed to work perfectly fine for me o.o

If you can share a replay that shows the issue and the .mpq that was used, I could try figuring it out from there.

Somehow I forgot to mention this (although I was sure I did mention it for some reason), but the only part that's not working (To be more descriptive: It upgrades properly, executes send_suicide 0 properly and then never attacks beyond the first attack;; it's easy to spot compared to the normal block since it doesn't expand) is the entire :fortress block and everything onwards - I just tested it on clean (other for P7A) patch_rt and it still doesn't work, so I'd guess you just encountered the non-randomised block?




Jun 28 2017, 2:16 pm Neiv Post #18



Nah, the non-expanding, prep_down variant worked perfectly fine as well when I played around with the script.



None.

Jun 28 2017, 2:53 pm Nekron Post #19



You know, I think this might be because it gets supply blocked really fast. I forgot about observers when adding up supply, and the defense_matrix block values are obviously just absurd, I think I counted reavers as 2 supply and carriers as 4 for some reason. After tommorow's exams I'll tweak the numbers a little bit and see if it works when it's not supply capped from the get-go.




Jun 29 2017, 11:20 am Nekron Post #20



Still doesn't work, I'm out of ideas as to why it wouldn't. I'm attaching the patch_rt. It probably needs a give_money somewhere in there to run for longer periods of time since staring mins are pretty low, but it's not the issue anyway - it stops working with 25k mins/15k gas still in the bank

Attachments:
patch_rt.mpq
Hits: 1 Size: 1835.5kb




Options
Pages: 1 2 35 >
  Back to forum
Please log in to reply to this topic or to report it.
Members in this topic: None.
[2024-4-14. : 9:21 pm]
O)FaRTy1billion[MM] -- there are some real members mixed in those latter pages, but the *vast* majority are spam accounts
[2024-4-14. : 9:21 pm]
O)FaRTy1billion[MM] -- there are almost 3k pages
[2024-4-14. : 9:21 pm]
O)FaRTy1billion[MM] -- the real members stop around page 250
[2024-4-14. : 9:20 pm]
O)FaRTy1billion[MM] -- look at the members list
[2024-4-12. : 12:52 pm]
Oh_Man -- da real donwano
da real donwano shouted: This is the first time I've seen spam bots like this on SEN. But then again, for the last 15 years I haven't been very active.
it's pretty common
[2024-4-11. : 9:53 pm]
da real donwano -- This is the first time I've seen spam bots like this on SEN. But then again, for the last 15 years I haven't been very active.
[2024-4-11. : 4:18 pm]
IlyaSnopchenko -- still better than "Pakistani hookers in Sharjah" that I've seen advertised in another forum
[2024-4-11. : 4:07 pm]
Ultraviolet -- These guys are hella persistent
[2024-4-11. : 3:29 pm]
Vrael -- You know, the outdoors is overrated. Got any indoor gym and fitness equipment?
[2024-4-10. : 8:11 am]
Sylph-Of-Space -- Hello!
Please log in to shout.


Members Online: Roy, jun3hong