Outpost Universe Forums

Projects & Development => Outpost 2 Programming & Development => Topic started by: Hidiot on September 12, 2007, 08:00:42 AM

Title: Multiple Questions Asked
Post by: Hidiot on September 12, 2007, 08:00:42 AM
So, I managed to get a map up and running (sorta) today. I just used the Plymouth starship map with some modifications to the map (added a few extra cliffs, placed fixed beacon sites with random bar numbers) and made a human player start.

//1. Now... I wanted a 2nd player (AI) to start (just start for now, he doesn't have to do anything) but not a single structure/unit shows up...
//2. I would like to know how I can use the _player functions, to set certain things like starting population, morale and maybe the color too.
//3. Also, I would need to know how to have cargo trucks start with metals and food in them.
//4. How to use daylight stuff?
//5. How to set lights for every unit to on?
//6. When I use the unit class to set the cargo to the trucks, only one of my trucks actually get filled. What should I do?
//7.I tried getting the AI to deploy a mine using some code I found in the AI programming,but it doesn't work. The Surveyor seems to survey the beacon, but the miner doesn't build... in fact, it just locks itself there like a ghost, units can get through it. How to fix this?
//8. I want this map to have random disasters, and I mean 100% random disasters. Random locations, random magnitude (for quakes) random intervals, etc...
But... I'm not sure what to use for randomizing values and I can't figure out how to use time triggers (any triggers for that matter) yet.
edit: Well... I changed my mind about the 100% randomness :D I've mapped out some safer areas, that'll have less destructive stuff... but no place will be 100% safe.
//9. How to get the AI trucks to mine?
//10. I attempted getting an eruption via a trigger, but I'm getting something wrong. How should I write a disaster that comes after a time trigger?
//11. Where can I find/get the code for a working eruption, using the basic stuff, and not the smartlava thing? I just want to get the map defined terrain-wise(including eruptions/blight). I will eventually use the SmartLava script to make it a bit more intricate, but for now I just want to get an eruption to go off at around mark 20, and the lava flow animation on the volcano to start at mark 10.
//12. If the AI has a structure kit pre-placed in its SF storage, will a ConVec take the placed one if it has to be build using the normal RecordBuilding, or will the SF build another kit and the ConVec use the newly built kit?
/13. Past attempts on avoiding the RecordBuilding function to help speed up the process of building the AI base has come to a stand-still, as I can't figure out a way to make it work. I've started using the IUnits header and have got the SF to produce a basic lab kit. But then's the problem... how do I get a next ConVec that has no cargo to the dock and load the SF cargo, whilst building a new kit in the meantime.
//14. How big does the reinforce group value difference hav eto be to force the VF to build something before the other things? Using the reinforceVehicleGroup to get the VF to make stuff, it won't always make both the Cargo Trucks it has to, building an Earthworker or a Repair Vehicle in the meantime...?
//15.I'm now trying to get the AI to respect certain research orders, but the way I tried it makes the game crash. Any suggestions?

16. Next real challenge will be optimizing building times for the AI. Record Building just doesn't cut it. No problems yet, but suggestions as to how I could do this are always welcome.


Warning: Content might cause head aches, severe migraines or a possible stroke to any experienced map coder!
Title: Multiple Questions Asked
Post by: BlackBox on September 12, 2007, 12:48:24 PM
Quote
1. Now... I wanted a 2nd player (AI) to start (just start for now, he doesn't have to do anything) but not a single structure/unit shows up...

2. I would like to know how I can use the _player functions, to set certain things like starting population, morale and maybe the color too.

3. Also, I would need to know how to have cargo trucks start with metals and food in them.

4. How to use daylight stuff?
I'll answer the questions in a slightly different order (the simple ones first):

4. This is pretty easy. Just do something like:
Code: [Select]
TethysGame::SetDaylightEverywhere(0);
TethysGame::SetDaylightMoves(1);

The first line shuts off daylight (changes it from being everywhere to just a single band). The second line allows the band to move (effectively turning on day and night mode).

2. Players can be accessed using the Player array, which is indexed from 0 to 6 starting with the first player, for example:
Code: [Select]
Player[0].SetWorkers(30); // give player 1 30 workers
Player[1].SetKids(200); // give player 2 200 kids

Morale is done slightly differently, using TethysGame:
Code: [Select]
TethysGame::ForceMoraleGood(1); // this will lock Player #2 to "Good" morale (75)

Change the 1 above to do it for a different player. Likewise, you can lock players to other levels (Rotten, Poor, Fair, and Great are the other ones).

You can unlock morale using:
Code: [Select]
TethysGame::FreeMoraleLevel(playerNum);

Note that if you do any morale locking / unlocking after the game has started (i.e. not tick 0), a "CHEATED GAME" message will appear in the upper left corner of the screen.

1. First off, in the DescBlock you need to change the player number to allow for more players:
Code: [Select]
SDescBlock DescBlock = { Colony, 2, 12, 0 }; // Important level details

Note the 2 instead of the 1 as in your code below. Change this to however many players you need (keep in mind that the game only allows 7 players, with the last player being mostly used for "gaia" / environment owned items).

As far as setting whether the AI player is Eden or Plymouth, use something like:
Code: [Select]
Player[1].GoEden(); // for eden
Player[1].GoPlymouth(); // for plymouth

You can do the same thing for Player[0] if you want to do it for the human player as well.

Furthermore, you can set the AI player to 'AI mode' if you wish:
Code: [Select]
Player[1].GoAI();
All this does is to give the player basically unlimited population (kids, workers, scientists are all set to 4096). It's sort of a cheat -- it's best to just let your AI player act as a human player and manage everything normally.

3. In your InitProc, define new Unit variables for the trucks:
Code: [Select]
Unit truck;

Then, for the CreateUnit calls for the trucks, for example to modify one of your trucks:
Code: [Select]
TethysGame::CreateUnit(truck, (map_id)1, LOCATION(125+31, 108-1), 0, (map_id)0, 4);

Simply change the 'x' to whatever you named the variable up above. This gives you a reference to the unit that you can use later to set the cargo as follows:
Code: [Select]
truck.SetTruckCargo(truckFood, 0); // to give it a cargo of food for example

Hope this answers your questions!
Title: Multiple Questions Asked
Post by: Hidiot on September 12, 2007, 03:20:56 PM
On compiling it gives me the following errors:

undeclared identifiers (Truckf and Trucko)


and:

"left of '.SetTruckCargo' must have class/struck/union type"

It identifies the truckf and trucko when it generates the trucks. It only does this on the cargo setting part.


EDIT: Stupid me... I was using the unit type uncaptioned  :heh:

Now it works.


Thanks BlackBox!
Title: Multiple Questions Asked
Post by: Hooman on September 12, 2007, 04:28:49 PM
You might also want to consider using the named unit types in the map_id enum. (Check "MapIdEnum.h"). Instead of using "(map_id)1", you can use "mapCargoTruck". Same with most of your other uses of the (map_id) cast.

The only one that really requires the cast, is for the tiger speed modification in the wreckage. If you're using the default tech tree, then you can use the TechID enum found in "OP2Helper\EnumTechId.h". (You're interested in the "techTigerSpeedModification" one). If you're not using the default tech tree then you're pretty much stuck with the way you currently have it, or making your own enum.


I'm sort of wondering if you're using some really old template code because of that actually. Or if you just happened to know enough C/C++ to do that to make the compiler stop complaining after getting an error about it.
 
Title: Multiple Questions Asked
Post by: BlackBox on September 12, 2007, 11:17:45 PM
Quote
You might also want to consider using the named unit types in the map_id enum. (Check "MapIdEnum.h"). Instead of using "(map_id)1", you can use "mapCargoTruck". Same with most of your other uses of the (map_id) cast.

The only one that really requires the cast, is for the tiger speed modification in the wreckage. If you're using the default tech tree, then you can use the TechID enum found in "OP2Helper\EnumTechId.h". (You're interested in the "techTigerSpeedModification" one). If you're not using the default tech tree then you're pretty much stuck with the way you currently have it, or making your own enum.


I'm sort of wondering if you're using some really old template code because of that actually. Or if you just happened to know enough C/C++ to do that to make the compiler stop complaining after getting an error about it.
It looks like code that was generated by the mapper -- the mapper doesn't have all the names for each of the enum values written it (it just outputs numbers). IIRC the cast was needed for certain things (for example specifying cargo in a cargo truck, you have to use values from the Truck_Cargo enum). I believe in some cases warnings would appear (by default, some MS compiler option I'm sure) as well if you used integer literals instead of the enum symbols which is why the casts are used.

The code generator is incredibly stupid as well. (Basically, it parses a file containing text blocks for each piece of code that it might want to generate, and performs simple text substutition on some piece of data in the input). It doesn't actually parse the code into any type of structure (and really there isn't a huge need to, considering that it doesn't need a lot of complexity).
Title: Multiple Questions Asked
Post by: Hidiot on September 14, 2007, 06:37:13 AM
Uh... I did use the mapper to set units/buildings/beacons...

I have added more questions in the first post and updated the code with some more modifications I've made.
I have marked answered questions with the "//" thing.
That's pretty much what I'm going to do until I'm finished...


Maybe the answers provided here will help other beginners?
Title: Multiple Questions Asked
Post by: Hooman on September 14, 2007, 06:30:49 PM
Code: [Select]
5. How to set lights for every unit to on?

6. When I use the unit class to set the cargo to the trucks, only one of my trucks actually get filled. What should I do?

7.I tried getting the AI to deploy a mine using some code I found in the AI programming,but it doesn't work. The Surveyor seems to survey the beacon, but the miner doesn't build... in fact, it just locks itself there like a ghost, units can get through it. How to fix this?

There is code to turn on the lights for every unit posted somewhere. I think it was in an earlier version of the SDK. I guess it should be added back in. Anyways, it's typically done in one of two ways. For both ways, you call <Unit>.DoSetLights(bool bOn). You can do it after you create each unit (which you are kind of doing), or you can use one of the Enum classes to loop over all the units, and call DoSetLights. With this later method, you won't need to make any change when adding or removing units, although it also offers less control over which units have their lights turned on or off.

Here's what's in your post:
Code: [Select]
TethysGame::CreateUnit(ConVec, (mapConVec), LOCATION(126+31, 107-1), 0, mapCommandCenter, 4);
 TethysGame::CreateUnit(ConVec, (mapConVec), LOCATION(126+31, 105-1), 0, (mapStructureFactory), 4);
 TethysGame::CreateUnit(ConVec, (mapConVec), LOCATION(125+31, 106-1), 0, (mapTokamak), 4);
 TethysGame::CreateUnit(ConVec, (mapConVec), LOCATION(127+31, 106-1), 0, (mapTokamak), 4);
 TethysGame::CreateUnit(ConVec, (mapConVec), LOCATION(124+31, 105-1), 0, (mapCommonOreSmelter), 4);
 TethysGame::CreateUnit(ConVec, (mapConVec), LOCATION(124+31, 107-1), 0, (mapAgridome), 4);
 TethysGame::CreateUnit(x, (mapEarthworker), LOCATION(126+31, 103-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(x, (mapRoboDozer), LOCATION(125+31, 104-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(RM, (mapRoboMiner), LOCATION(124+31, 103-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(RS, (mapRoboSurveyor), LOCATION(123+31, 104-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(Trucko, (mapCargoTruck), LOCATION(125+31, 108-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(Truckm, (mapCargoTruck), LOCATION(123+31, 108-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(Truckf, (mapCargoTruck), LOCATION(123+31, 106-1), 0, (map_id)0, 4);
...
       ConVec.DoSetLights(1); Truckf.DoSetLights(1); Trucko.DoSetLights(1); Truckm.DoSetLights(1); RS.DoSetLights(1); RM.DoSetLights(1);

The problem with the above, is the ConVec unit reference gets overwritten each time you use it. If you look at the first 5 units created, they all store their reference into the ConVec variable, overwriting any previous value. What you can do is put the DoSetLights calls inbetween the CreateUnit calls, so you set the lights for that unit before you overwrite the reference to it.

[Side note: the Unit class you use in your DLL isn't the same as what's used inside the game engine. The exported class for use in DLLs contains only one 4 byte int, which is just the index of the internal unit it refers to. Overwriting a variable of this type or destroying one has no affect on the actual unit in the game. The internal unit class has room for 120 bytes to store all the unit info, such as it's current position, it's hitpoints, cargo/weapon, current animation, etc.]

The other method [I'm not testing this code, or even compiling it to check for syntax errors] can set the lights of all units, and is basically done like this:
Code: [Select]
Unit unit;
PlayerVehicleEnum vehicleEnum(0);  // Player 0
while(vehicleEnum.GetNext(unit) != 0)
    unit.DoSetLights(true);

You can also throw that into a larger loop that will set the lights for all players. That chunk of code only does it for all units of a given player.


As for setting the cargo, it looks like you're doing it correctly here:
Code: [Select]
 TethysGame::CreateUnit(Trucko, (mapCargoTruck), LOCATION(125+31, 108-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(Truckm, (mapCargoTruck), LOCATION(123+31, 108-1), 0, (map_id)0, 4);
 TethysGame::CreateUnit(Truckf, (mapCargoTruck), LOCATION(123+31, 106-1), 0, (map_id)0, 4);
       Truckf.SetTruckCargo(truckFood, 1000);
       Trucko.SetTruckCargo(truckCommonOre, 1000);
       Truckm.SetTruckCargo(truckCommonMetal, 1000);
Each created unit is stored into a new unit variable, so you're not overwriting the reference before using it to set the cargo. You could also have interleaved the calls to CreateUnit and SetTruckCargo, which would only require one variable. (But most people find that looks a bit messy, and is harder to find stuff and edit).

Is this the way you had it before when you had problems? Or are you still having problems with this one?


I'm not too sure about the mine building. There is a difference between how a ConVec and a RoboMiner build though. If you mix up the commands, than the unit might go to the wrong place, and not build properly. Remember that a robo miner builds right over the mine, but a ConVec needs to position itself below right of the structure. I remember some oddities where you can get a robo miner to build a mine if you played around with the coordinates to match the location a ConVec needs to be, but this is kind of a hack and is a bit ugly. I think that was using DoDevelop to build a mine, using ConVec coordinates. It looks like you may be using ConVec coordinates with the building group way though. Plus I notice you have a commented out DoDevelop. Maybe try adjusting the coordinates back on top of the mine.

If that fails, I know of another method, but I don't feel it's very appropriate. I think getting the BuildingGroup to work for you is probably the best option.
Title: Multiple Questions Asked
Post by: Hidiot on September 15, 2007, 07:37:53 AM
Ok... the light now work.


I tried putting common metals in both Cargo Trucks and it didn't work back then, so I just filled one with ore to get over the problem. But I think I could fill both trucks with metals now with your help.


The DoDevelop doesn't work either. The miner just goes there and does nothing... and it goes directly over the beacon.  I'm going to try moving its location a bit and see what happons....
EDIT: I put the Robo-Miner on the neighboring lower-right tile and it worked this time.
After I finish fixing certain things I did with the tech tree I'll get back to working o nthe map on getting the AI do do stuff. Next thing on the list will be getting 2 trucks to haul ore from the mine to the smelter and get a ConVec to fix those Tokamaks from time to time.


Also, updated the first post with latest code state and question(s).

2nd EDIT: Found the library with the triggers and I think I can use them... I quite understand what they need to work. NOw I need to find out what I should use for randomizing values (I have some ideas, but I want to be sure).
Title: Multiple Questions Asked
Post by: Hooman on September 15, 2007, 02:47:13 PM
Make sure you use the built in randomization. Just make calls to TethysGame::GetRand(int range). If you try to use some other random number generator in a multiplayer game, the games will desync. It's just good habit for single player, and there's really no reason to use something else anyways.


You can use a MiningGroup to get the ore hauling going. There's some important stuff on that that's already posted somewhere. Basically make sure to set their rect properly, so they go back to the smelter and wait when they're full and you can't store any more ore.
 
Title: Multiple Questions Asked
Post by: Hidiot on September 15, 2007, 03:03:37 PM
Yeah,uh... how do I set that damn rect? I can't seem to make it work :|
Title: Multiple Questions Asked
Post by: Hooman on September 16, 2007, 02:38:54 AM
I believe it's top left inclusive, and bottom right exclusive. So a rect of (0, 0, 0, 0), is an empty rect, and (0, 0, 1, 1) has exactly one tile in it. Put that rect around the dock of your smelter, and make it a little bigger. If you put it elsewhere, then your trucks will go to some weird location to wait when the smelter is full. If you make the rect too small, then the cargo trucks will fight each other to get into the rect, and keep pushing each other out. It's kind of annoying when they don't settle down, or head off to some obscure location.

So yeah, basically find where the dock of the smelter is, and set the rect to start a little bit left and above it, and end a little right and below it, and make sure it's big enough to hold a few tucks around the pad, that aren't on it.
 
Title: Multiple Questions Asked
Post by: Hidiot on September 16, 2007, 04:27:57 AM
Ah, so the rect determines where the trucks wait... I see... I'll try again and see if I can make it work now...


Also, if the questions I ask here about how to get the AI to do stuff should be moved to the AI Coding forum, I will make a different topic there to continue my... annoying questions.

edit: Hmmm... I'm going something wrong, cause the game crashes because of what I did with the mining. Updated code...
Title: Multiple Questions Asked
Post by: Hooman on September 16, 2007, 01:46:58 PM
Get rid of your for loop. The while loop is what you need. What you have is a doubly nested loop, and it might be adding the trucks twice. (Or just doing nothing the second time). It's basically running the whole while loop twice (once for j =0, and once for j=1).

If that doesn't work, maybe change the order of the function calls to setup the group. It might matter if you set the rect before or after you add the units to the group. Maybe not though.


Edit: Moved thread to AI Coding forum.
 
Title: Multiple Questions Asked
Post by: Hidiot on September 17, 2007, 08:37:37 AM
Updated initial code and questions.

Changing the place where the RECT goes, doesn't work. The game stil lcrshes before it starts.

I really need an example of a working trigger, cause I'm not sure what I'm using is actually working...

And,uh... I think it's better to split this topic... I'll be having AI questions, that's for sure, but I'm still not done with the coding part, mainly disasters.
Title: Multiple Questions Asked
Post by: Hooman on September 17, 2007, 06:04:03 PM
Ok, I found a slight error in my SDK header file. In case anyone else has run into this, the correct definition of (the last overloaded versions of) MineGroup.Setup is:
Code: [Select]
	void Setup(class Unit mine, class Unit smelter, struct MAP_RECT &smelterArea);

The mine and smelter parameters were reversed. I noticed that when I tried to setup a mining group and the empty cargo truck headed straight to my smelter and just sat there.

Some code from a working example is:
Code: [Select]
	Unit commandCenter;
TethysGame::CreateUnit(commandCenter, mapCommandCenter, LOCATION(40, 7), 0, mapNone, 0);
Player[0].CenterViewOn(45, 10);
Player[0].SetFoodStored(10000);
Player[0].SetScientists(10);
Player[0].SetOre(9000);

Unit mine, smelter, cargoTruck1, cargoTruck2;
TethysGame::CreateBeacon(mapMiningBeacon, 50, 10, 0, 0, -1); // [(50, 10), common, 3 bar, random variant]
TethysGame::CreateUnit(mine, mapCommonOreMine, LOCATION(50, 10), 0, mapNone, 0);
TethysGame::CreateUnit(smelter, mapCommonOreSmelter, LOCATION(40, 10), 0, mapNone, 0);
TethysGame::CreateUnit(cargoTruck1, mapCargoTruck, LOCATION(47, 10), 0, mapNone, 0);
TethysGame::CreateUnit(cargoTruck2, mapCargoTruck, LOCATION(44, 11), 0, mapNone, 0);

MiningGroup &mineGroup = CreateMiningGroup(Player[0]);
//mineGroup.Setup(mine, smelter, MAP_RECT(40, 9, 42, 12));
mineGroup.Setup(mine, smelter, MAP_RECT(40, 10, 43, 13));
mineGroup.TakeUnit(cargoTruck1);
mineGroup.TakeUnit(cargoTruck2);

Just dump that in InitProc to watch that in action. I set the initial ore to 9000, so it'll fill up after the two trucks unload, and then on the second trip they'll stop and wait by the smelter with their loads.

Note that on my first attempt, I choose a bad rect, and so I had one cargo truck circling one side of the smelter. If you make the rect too big, they might stop nowhere particularly close to your smelter.





Volcanos
----------

I just took a look at CES1.dll to see how they did it, and tried something similar.

Place something like this in InitProc
Code: [Select]
	// Make the vent appear in 1 mark (1 mark = 100 ticks)
Trigger &timeTrigger = CreateTimeTrigger(true, true, 100, "VolcanoVent");

Then add something like these two functions to your source file:
Code: [Select]
SCRIPT_API void VolcanoVent()
{
// Make the vent appear (just changes map tiles)
// (Appearance shortly before eruption is detectable)
GameMap::SetTile(LOCATION(165+31, 154-1), 1140);
GameMap::SetTile(LOCATION(165+31, 155-1), 1150);

// Start the eruption on it's way in 1 mark
Trigger &timeTrigger = CreateTimeTrigger(true, true, 100, "Eruption");
}

SCRIPT_API void Eruption()
{
// Mark where the lava can flow
// (do this anytime before the eruption, including from InitProc)
GameMap::SetLavaPossible(LOCATION(165+31, 156-1), true);
GameMap::SetLavaPossible(LOCATION(163+31, 157-1), true);
GameMap::SetLavaPossible(LOCATION(164+31, 157-1), true);
GameMap::SetLavaPossible(LOCATION(165+31, 157-1), true);
GameMap::SetLavaPossible(LOCATION(166+31, 157-1), true);

// Make the vent appear (just changes map tiles)
// (Appearance when eruption is detectable)
GameMap::SetTile(LOCATION(165+31, 154-1), 1147);
GameMap::SetTile(LOCATION(165+31, 155-1), 1158);

// Start the lava flowing  (... in about 10 marks)
// At this point, the earliest disaster warning will sound
TethysGame::SetEruption(165+31, 156-1, 150);
}

In CES, the vent first appears (bubbling flow) at mark 50 (first timer set to 5000), it stays that way for 15 marks (second timer set to 1500), then at mark 65 it switches to the solid flow and sets the eruption. If you had early disaster warning, you'd hear about the volcano at this point. Then after 10 marks (standard time between when disasters are created, and when they first appear, giving time for the player to hear the first and second disaster warnings) the eruption finally happens. There is no way to change that 10 mark delay with the exported functions (but it can be done with some memory editing).

In the example, I wait 1 mark before displaying the lava vent, then another 1 mark before changing the graphic to a solid flow and setting the eruption. After another 10 marks (at mark 12 now) the eruption finally happens.
 
Title: Multiple Questions Asked
Post by: Hidiot on September 18, 2007, 03:53:43 AM
updated code.

The mine I use here is deployed by a miner, so that code won't work.. .and the LOCATION one didn't work... it just crashes the game if I leave the mining code in...
edit: Tried it with a time trigger, and it will still just crash the game.

Is there no code to get 2 trucks to mine a deployed mine? Must the mine start deployed? If so, how do you get the AI to make working Rare ore mines? :|



I made the volcano erupt and I've set a few tiles for it to move on, but it just won't move... not even on its 2 adjunct tiles specified individually.
Title: Multiple Questions Asked
Post by: Hooman on September 18, 2007, 11:20:16 PM
It can take a while for the lava to expand. Lava and blight expansion is a little weird. Basically when you set the spread spead, it determines how many tiles per expansion cycle are checked for possible expansion. The number you specify as a parameter gets scaled based on the map size, so it should end up giving the same rate for different sized maps. The tiles that are checked are basically random ones all over the map (they duplicated the random number generator code exactly for much of it). It checks the specified number of tiles, generating the (x,y) coordinates randomly, and sees if it's next to other blight or lava and is a possible point of expansion. That's partly why it grows in weird trails and is sort of vine/finger like sometimes, and not just solid growth outwards in a circle. Plus the random number generator is strangely deterministic and predicatable, to a point of actually being physically noticable. Although it is still possible that it'll be a while before a tile adjacent to your starting position is checked for expansion, so it can take a while to start spreading.

The point of eruption should be marked as a place where lava is possible. If you don't mark it, then lava can't be placed when the eruption happens, and so there will also be no lava to spread to adjacent tiles. The animation of lava comming out of the volcano is not set to lava possible. If you did that, then the animation would get overwritten with the ugly lava tiles, and look really weird.


Btw, the game DLLs tend to use special code and structs for marking lava possible. If you want to do it the same way, or just an example, here's how they do it. It seems to store the info sort of in scanlines, where you'd have a y coordinate, and then two x coordinates for the start and end of the line along that y. It loops between the starting x value and the ending x value, and marks all those tiles with lava possible. It also loops over all these structs, doing that for each of the scanlines, and eventually it has an entire region marked that can be filled with lava. Well, almost, except the game seems to use vertical strips if I remember right instead of horizontal strips, so maybe scanline isn't a good word to use, but it's the same idea. Note that the game just stores the lava possible as a bit in the map data. (The map data has 32 bits per tile. It uses 5 bits per cell type, 11 bits per tile index, 11 bits per unit index of a unit occupying that tile, and various bit flags, in no particular order, for tiles occupied by a building or are otherwise unbuildable, lava possible, actual lava presence, and blight presence). In other words you just need an easy way to set the lava possible bit for the tiles you want, and there is no real reason to do it like it's currently being done. You could even do this in a map editor (that supports it), since the in memory format of the map data is the same as the disk format.

The code would look something like this:
Code: [Select]
struct LavaScanline
{
  int y;
  int x1;
  int x2;
};

void SetLavaPath(int numStructs, struct lavaScanline)
{
  for (i = 0; i < numStructs; i++)
    for (j = lavaScanline.x1, j <= lavaScanline.x2; j++)
      GameMap::SetLavaPossible(j, lavaScanline.y, true);
}



// Defining data (might want to check if this is even valid C++ code)
LavaScanline lavaScanline[] = {
  { 40, 50, 55 },
  { 41, 49, 56 },
  { 42, 47, 56 }.
  { 43, 46, 58 },
  // ...
};

// Maybe use an expression like this to calculate the size of the array,
// and possibly a macro to output both the array size and pointer,
// much like what is used in OP2Helper for the base data
int numLines = size(lavaScanline) / sizeof(lavaScanline[0]);  // sizeOfArray/sizeOfElement
// Calling the function
SetLavaPath(numLines, lavaScanline);


Edit: Oh, and don't forget that I only posted tile indexes for eruptions in one direction. There are other tiles you can use too. Like eruptions on the left, middle, and right side of a volcano.


I'm pretty sure there is a way to do the mining right, like you wanted, but I'll probably have to wait till the weekend to find the time to look into it. Make sure to bump this thread around that time.
Title: Multiple Questions Asked
Post by: Hooman on September 23, 2007, 01:50:05 AM
Ok, I got some working example mine code based off of CES1.dll. What they did is setup the mining using a repeated time trigger. The time trigger would fire every 97 ticks, and would check for the presence of the right kind of smelter. Once the smelter existed, they setup the mining group and destroyed the time trigger (or reused it to trigger a different even, thus essentially destroying it for it's previous purpose).

I don't think the whole trigger setup is actually necessary, but I figure I'll post code that does it that way. Mostly because I was following how CES1.dll did it, and when I got it working the code was in that form.

Also, the current form of the code uses global objects in the DLL, which will need to be saved correctly if the user Loads/Saves the level. If you didn't setup the SaveRegions properly while using global variables, they wouldn't have their values restored when you loaded the game, and who knows what would happen then. Things might just stop working, or the game might crash. At any rate, I get to demonstrate an example of that too.

One fine point to note about variables in C++ is that the language doesn't specify the order of variables in memory in relation to the order they are declared. They usually get stuck in memory in the same order they are declared, but this is not a requirement, and there are some research projects that attempt to reorder the variables in memory for various purposes. Sometimes it's to do with speed and cache locality, and sometimes it's for security reasons. There is one way to force the ordering however. You just put the variables into a struct. That way you know exactly where all the variables go, and can easily get the size of all of them. This is important for SaveRegions since we just get to save one contiguous chunk of memory, and we'll want exactly the variables we specify to be in that chunk.


The code:
Code: [Select]
// Global Script variable layout
struct ScriptGlobal
{
BuildingGroup buildGroup;
Trigger mineGroup1TimeTrigger;
Trigger mineGroup2TimeTrigger;
Unit supernovaLynx;
};

// Global Script variables
ScriptGlobal scriptGlobal;


int InitProc()
{
// Bulldoze smelter area for faster testing
int i, j;
for (j = 4; j < 14; j++)
{
  for (i = 41; i < 50; i++)
  {
   GameMap::SetCellType(LOCATION(i, j), 21);
   GameMap::SetTile(LOCATION(i, j), 1670);
  }
}

// Set initial colony status
TethysGame::ForceMoraleGreat(-1);
Player[0].MarkResearchComplete(techEfficiencyEngineeringEden);
Player[0].MarkResearchComplete(techCyberneticTeleoperation);
Player[0].GoAI();
Player[0].GoEden();
Player[0].CenterViewOn(45, 10);
Player[0].SetFoodStored(10000);
Player[0].SetWorkers(10);
Player[0].SetScientists(10);
Player[0].SetOre(10000);

// Create an initial base to support the testing
Unit commandCenter, structureFactory, vehicleFactory, tokamak, commonStorage;
TethysGame::CreateUnit(commandCenter, mapCommandCenter, LOCATION(40, 7), 0, mapNone, 0);
TethysGame::CreateUnit(structureFactory, mapStructureFactory, LOCATION(36, 7), 0, mapNone, 0);
TethysGame::CreateUnit(vehicleFactory, mapVehicleFactory, LOCATION(36, 11), 0, mapNone, 0);
TethysGame::CreateUnit(tokamak, mapTokamak, LOCATION(36, 2), 0, mapNone, 0);
TethysGame::CreateUnit(commonStorage, mapCommonStorage, LOCATION(39, 10), 0, mapNone, 0);
TethysGame::CreateUnit(commonStorage, mapCommonStorage, LOCATION(41, 10), 0, mapNone, 0);

TethysGame::CreateUnit(scriptGlobal.supernovaLynx, mapLynx, LOCATION(45, 4), 0, mapSupernova, 0);

// Create a mining beacon
TethysGame::CreateBeacon(mapMiningBeacon, 50, 10, 0, 0, -1); // [(50, 10), common, 3 bar, random variant]

// Create a building group
BuildingGroup &buildGroup = CreateBuildingGroup(Player[0]);
buildGroup.SetRect(MAP_RECT(33, 5, 39, 10));
buildGroup.TakeUnit(structureFactory);
buildGroup.TakeUnit(vehicleFactory);
buildGroup.RecordBuilding(LOCATION(45, 10), mapCommonOreSmelter, mapNone);
buildGroup.SetTargCount(mapConVec, mapNone, 1);
buildGroup.RecordBuilding(LOCATION(50, 10), mapCommonOreMine, mapNone);
buildGroup.RecordBuilding(LOCATION(45, 6), mapCommonOreSmelter, mapNone);
buildGroup.RecordVehReinforceGroup(buildGroup, 1000);

// Save building group reference in global
scriptGlobal.buildGroup = buildGroup;

// Check periodically for the smelters being built
scriptGlobal.mineGroup1TimeTrigger = CreateTimeTrigger(true, false, 97, "SetupMining1");
scriptGlobal.mineGroup2TimeTrigger = CreateTimeTrigger(true, false, 97, "SetupMining2");

return 1; // return 1 if OK; 0 on failure
}

void AIProc()
{
if ((TethysGame::Tick() > 7500) && (scriptGlobal.supernovaLynx.IsLive()))
{
  scriptGlobal.supernovaLynx.DoSelfDestruct();
}
}

void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = &scriptGlobal;
bufDesc.length = sizeof(scriptGlobal);
}

int StatusProc()
{
return 0;
}

SCRIPT_API void NoResponseToTrigger()
{
}


SCRIPT_API void SetupMining1()
{
Unit smelter;
PlayerBuildingEnum buildingEnum(0, mapCommonOreSmelter);
LOCATION smelterLoc(45, 10);

// Check for a smelter at the desired location
for (;;)
{
  if (buildingEnum.GetNext(smelter) == 0) return;
  LOCATION &temp = smelter.Location();
  if ((temp.x == smelterLoc.x) && (temp.y == smelterLoc.y)) break;
}

// Setup a mining group, which will be later reinforced
MiningGroup &mineGroup1 = CreateMiningGroup(Player[0]);
mineGroup1.Setup(LOCATION(49, 10), LOCATION(46, 11), mapCommonOreMine, mapCommonOreSmelter, MAP_RECT(45, 10, 49, 14));
mineGroup1.SetTargCount(mapCargoTruck, mapNone, 2);

// Make the building group reinforce the mining groups
scriptGlobal.buildGroup.RecordVehReinforceGroup(mineGroup1, 100);

// Done with the trigger, make sure we don't reenter
scriptGlobal.mineGroup1TimeTrigger.Destroy();
}

SCRIPT_API void SetupMining2()
{
Unit smelter;
PlayerBuildingEnum buildingEnum(0, mapCommonOreSmelter);
LOCATION smelterLoc(45, 6);

// Check for a smelter at the desired location
for (;;)
{
  if (buildingEnum.GetNext(smelter) == 0) return;
  LOCATION &temp = smelter.Location();
  if ((temp.x == smelterLoc.x) && (temp.y == smelterLoc.y)) break;
}

// Setup a second mining group, which will be later reinforced
MiningGroup &mineGroup2 = CreateMiningGroup(Player[0]);
mineGroup2.Setup(LOCATION(49, 10), LOCATION(46, 7), mapCommonOreMine, mapCommonOreSmelter, MAP_RECT(45, 7, 49, 11));
mineGroup2.SetTargCount(mapCargoTruck, mapNone, 2);

// Make the building group reinforce the mining groups
scriptGlobal.buildGroup.RecordVehReinforceGroup(mineGroup2, 100);

// Done with the trigger, make sure we don't reenter
scriptGlobal.mineGroup2TimeTrigger.Destroy();
}

Description:
Player 0 is set to GoAI, so you can't issue any commands to the units. You start with a base consisting of a CC, SF, VF, Tok, and two Common Stoage. I also added a nova lynx that is set to self destruct at Mark 75 (see AIProc). I added a variable to the globals section to keep the reference around for the self destruct command.

I added a BuildingGroup to setup the following. It builds the kits for two Common Ore Smelters, it builts a Robo Miner, it deploys the Robo Miner, and it builds a Convec. The Convec will pick up the kits from the SF and deploy the two Common Ore Smelters.

The two time triggers setup two MiningGroups, one for each smelter, and they share a common mine. The time trigger checks periodically for the smelter to see when it's there (initialled placed), and once it detects this, it sets up the mining groups, sets a target count of 2 cargo trucks for each group, and sets the BuildingGroup to reinforce the MiningGroups, so those cargo trucks will be built as they are needed at the VF.


The second smelter should finish building at about mark 70, and everything will be harvesting. Then at mark 75, the nova lynx explodes. This should take out the top smelter, and possibly a cargo truck or two. When this happens, the SF should start to build a new Common Ore Smelter kit, and the Convec should move to the pad to await pickup, and the VF should start to replace any lost Cargo Trucks. Once the kits is ready, the Convec will deploy it, and when it's finished building the cargo trucks will resume harvesting. This all happens automatically with the groups. Basically you just set them up and forget about them. As long as the building group is capable of replacing lost buildings and groups, it will. And as long as you have a mine and smelter operational at the correct location, mining will continue.


There is one important point about the mining Setup call. The locations you specify MUST be the EXACT coordinates of the DOCKs. These will not usually be the same as the coordinates used to build them. You must pay attention to where the docks are, or the trucks won't head to their destination. The internal code for the Groups actually does an exact compare on the coordinates it's given in Setup, and the dock location (using the internal function GetDockLocation) to see if they are an exact match.


I suspect, but have not yet tested, code that calls Setup using the overloaded location form will work even if you call setup well before the building is built. It seems like it should work that way, since when the smelter is destroyed and rebuilt, mining will continue with the new smelter. If this is the case then you can really get away without using the time triggers and without using the global variables. You just have to make sure the coordinates are the exact coordinates of the dock for each building.
 
Title: Multiple Questions Asked
Post by: Hooman on September 23, 2007, 06:27:15 PM
Ok, I tested out my idea, and it works just fine if you stick everything in InitProc. It works exactly the same in all cases (mostly). The only difference is the cargo trucks get built a little earlier, since the build group is aware it needs to build them earlier. I believe it selects a random order to build the vehicles in. This means the cargo trucks can be built before the convec or robo miner, which is maybe a little undesirable. That might slow down the overall buildup if it doesn't built what it needs by the time it needs it. I'm pretty sure you can get the old behavior by just putting the SetTargCount or RecordVehReinforceGroup calls into a time trigger, or delaying them by some other means. The Setup function seems quite happy to be run at any time.


This does sort of make setting up multiple bases and expansion somewhat easier. Just need to time when the initial units needed to start the building process are added to the groups.


For the new test, I modified the end of InitProc to be the following:
Code: [Select]
	// Create a building group
BuildingGroup &buildGroup = CreateBuildingGroup(Player[0]);
buildGroup.SetRect(MAP_RECT(33, 5, 39, 10));
buildGroup.TakeUnit(structureFactory);
buildGroup.TakeUnit(vehicleFactory);
buildGroup.RecordBuilding(LOCATION(45, 10), mapCommonOreSmelter, mapNone);
buildGroup.SetTargCount(mapConVec, mapNone, 1);
buildGroup.RecordBuilding(LOCATION(50, 10), mapCommonOreMine, mapNone);
buildGroup.RecordBuilding(LOCATION(45, 6), mapCommonOreSmelter, mapNone);
buildGroup.RecordVehReinforceGroup(buildGroup, 1000);

// Setup a mining group, which will be later reinforced
MiningGroup &mineGroup1 = CreateMiningGroup(Player[0]);
mineGroup1.Setup(LOCATION(49, 10), LOCATION(46, 11), mapCommonOreMine, mapCommonOreSmelter, MAP_RECT(45, 10, 49, 14));
mineGroup1.SetTargCount(mapCargoTruck, mapNone, 2);
// Make the building group reinforce the mining groups
buildGroup.RecordVehReinforceGroup(mineGroup1, 100);

// Setup a second mining group, which will be later reinforced
MiningGroup &mineGroup2 = CreateMiningGroup(Player[0]);
mineGroup2.Setup(LOCATION(49, 10), LOCATION(46, 7), mapCommonOreMine, mapCommonOreSmelter, MAP_RECT(45, 7, 49, 11));
mineGroup2.SetTargCount(mapCargoTruck, mapNone, 2);
// Make the building group reinforce the mining groups
buildGroup.RecordVehReinforceGroup(mineGroup2, 100);

return 1; // return 1 if OK; 0 on failure

I also deleted the two trigger callback functions, and gutting a few members from the ScriptGlobal struct, since I wasn't using them anymore. I only left the nova lynx entry, since I still wanted to test what happens when the smelter is destroyed.
 
Title: Multiple Questions Asked
Post by: Hidiot on September 24, 2007, 01:37:27 AM
I'll have to get to integrating these things in my map when I get the time :| I was rather sick this weekend and couldn't do very much...

EDIT: I finally got what I was doing wrong. The Setup on the mining group caused the game to crash, cause I didn't specify what buildings are at the locations I fed it.

I've found and am looking into McShay's demo for a working AI and am progressing slowly now. Got the code a tad tidier, setup the groups and preparing to test if I can make the AI build a new structure.

More work to be done... when I get the time :|


Almost forgot... updated code and a new question... more questions that is...


next EDIT: More updating on the code. I go the AI to expand a bit, so I can finally make it build a main base. Will be looking into making it build a 2nd base tomorrow. I've copied something that I've put in the AIProc from Mcshay's working AI example that I will try to edit and adapt tomorrow to my map.

Until I got the AI base planned, it will look and be built very unrealistically.  Once I get it looking and working like a base, I will ponder more on the time certain building should be built.
Title: Multiple Questions Asked
Post by: Hidiot on October 14, 2007, 09:02:40 AM
Ok, so it's been a while now, progress has been and will be slow. I will update the code later, cause I'm out of time right now.

More questions added in the hope I won't have to try my luck at coding what I need.
Title: Multiple Questions Asked
Post by: Hooman on October 14, 2007, 11:25:01 AM
Quote
12. If the AI has a structure kit pre-placed in its SF storage, will a ConVec take the placed one if it has to be build using the normal RecordBuilding, or will the SF build another kit and the ConVec use the newly built kit?

No. It will just build a new one to place. Not too sure why, but it only uses stuff that it produces itself.


Quote
13. Past attempts on avoiding the RecordBuilding function to help speed up the process of building the AI base has come to a stand-still, as I can't figure out a way to make it work. I've started using the IUnits header and have got the SF to produce a basic lab kit. But then's the problem... how do I get a next ConVec that has no cargo to the dock and load the SF cargo, whilst building a new kit in the meantime.

Yes.... That is quite a problem. It's easy to gloss over it with high level details, but when it comes to implementing something like that, it's usually somewhat more diffifcult in practice. There are a lot of details that you'd have to deal with. I'd say just figure out how to use RecordBuilding, since that's only one line, and doing it another way could easily take a few hundred to get it working the way you want it to. Granted, it could be a little more streamlined than the BuildingGroup works. I've noticed it won't start building the next structure kit until the current kit is placed. But still, I have no problem relying on this one considering the alternative.


Quote
14. How big does the reinforce group value difference hav eto be to force the VF to build something before the other things? Using the reinforceVehicleGroup to get the VF to make stuff, it won't always make both the Cargo Trucks it has to, building an Earthworker or a Repair Vehicle in the meantime...?

I don't think you can force that for certain. It would probably just change the probabilities of what will be built next. What you can do is delay setting the target counts for the vehicles that you don't want until later. Just setup a time trigger, and after enough time has passed to build the important stuff first, add the target counts for the other vehicles.

You can do something similar with RecordBuilding if you want the base to be built in steps. CES does that. The initial base it built up a little further at successive time intervals. After each of those marks, the SF, convecs, and earthworkers start on the next expansion part.
 
Title: Multiple Questions Asked
Post by: Hidiot on October 14, 2007, 01:48:38 PM
ok, so about 13... I'm willing to give it a try on the complicated part. If I can get an example running, then maybe it will be easier in the future.
Title: Multiple Questions Asked
Post by: Hooman on October 15, 2007, 02:39:04 AM
OK, but you're on your own! :P

Honestly, I don't think the built in method is that bad. Besides, you don't really want something that's too perfect usually, just because the player can't play that perfectly. It slows down the computer a bit and makes it a bit more fair. Mind you, a practised player could probably build a little faster provided they didn't have too many other thigns to worry about.


I know how to issue the orders to the units. I also know how to check most properties of units. You can also use enums to find the units. However, if you're constantly scanning for units, and testing for all the properties you want, it's gonna be kind of slow. I think what you'd really need is a good way to maintain an active list of units, and keep track of what they're doing. I don't currently know of a very good way of doing this. I've considered this problem to some extent though. What might be a partial solution, is to use the ScStub system, possibly creating your own objects compatible with the internal ones. They seem to have callbacks for certain events which you could then hook into. The main one I was interested in was the event that's raised when a unit is produced at the vehicle factory. This seems to be used to add units to the appropriate group (MiningGroup, BuildingGroup, etc.) when they are built. I've never actually written any code to play with this idea however. Plus, there is the other side of things, and keeping track of units that get destroyed. I don't currently know of a good way of doing that. I would hope there is something better than scan the list periodically for ones that died, because they could just get replaced by a new unit if the timing is just right. I guess the groups need some way of keeping track of this. I suppose I could look into it sometime.
Title: Multiple Questions Asked
Post by: Hidiot on October 16, 2007, 10:25:32 PM
Finally updated the script... worked on it, doing stuff and trying new things...

But I now run into a major problem. and that's question 15.

Tried removing the Player[1].GoAI, but that didn't help.


Also, I'm not sure I fully got what you said... And I doubt I can make it work on my own :/ I just want something that works faster than the RecordBuilding and that allows me to replace buildings when needed (for example, replace residences or the basic lab with med centers).
Title: Multiple Questions Asked
Post by: Hooman on October 17, 2007, 05:15:03 AM
Ok, first of all, using record building will build the buildings in the order you record them in. If a buildnig is destroyed, it will rebuild the one that's first in it's list before continuing on.

The built in levels usually record buildings in batches. It takes a lot less code that way. You seem to be using triggers very excessively in your code. Just add them in batches, and they will be built in the order they were recorded in. The only reason to really use time triggers, is to control batch buildings, where there is a fairly lengthy delay in the build up. Such as, the main base is expanded from it's initial form, then a second base is built, and then the main base is exapnded further, at some later point after the expansion base is setup, then maybe another base is built up, and then the main base starts expanding further. If you recorded all buildings at once, each building group would build in parallel, trying to finish (almost) as fast as it could. By delaying the record building for a batch, you can add delays between when one expansion is finished and the next is started. There is no point to adding a record building as soon as the previous building is done, as it would have the same effect as them both being recorded at the same time.
 
Title: Multiple Questions Asked
Post by: Hidiot on October 17, 2007, 10:02:50 AM
The time triggers are currently just there to get a feel of the AI... I plan on using Research triggers a lot, cause that's pretty much how a human player does it... build stuff just as it has researched it.

I'll look into the RecordBuilding order next time I get the time.


Buut... in order to test the map further, I need a fix to the researching problem. What's wrong with it, that the game crashes right when the first research should begin?


EDIT!: I've worked around with the researching and have come to the latest version. Check it just before the InitProc()...

Unfortunately, I can't tell if it is working or not. It has made the game stop crashing, but I fear there is no progress being done at all now :/
Title: Multiple Questions Asked
Post by: Hooman on October 18, 2007, 07:03:57 AM
I just noticed this one line here:
Code: [Select]
if (Player[1].HasTechnology(2701)) TethysGame::AddMessage(1+31,1-1,"IT IS WORKING!",0,0);

This seems sort of silly to have in InitProc, since it only ever gets run once at level startup. If you set the player to have that tech, then it'll print, otherwise it won't. You won't get a message later on in the game when they research it. (You can use a research trigger for that).


Also, you're code is getting rather large, and also kinda far back in the thread, making it a little harder to reference. It would help if you'd include the relevant code in replies, and try to keep it down to a minimum of the area we need to focus on. (We still need to see how things are defined and all, but maybe cut out stuff irrelevant to the current topic).


I'll look back later on. I'm pretty tired right now. Seriously overworked with school. :(
 
Title: Multiple Questions Asked
Post by: Hooman on October 19, 2007, 04:53:06 AM
Ok, there are a few issues I can see.

First of all, Player.Scientists() returns the total number of scientists, not the available number of scientists. You could still be trying to assign too many.

Secondly, lift the check for Scientists() > 0 out of that mess, since it's common to all. It would make the code much more readable.

Replace your rather sick looking nested if statements with something simpler. Generally you should use { } to disambiguate things, rather than "else;". What you can do is use (or create) a "min(int a, int b )" type of function. Better yet, if the library you're using allows you to quesry a tech for the max number of scientists for a research, is to combine this with a min function and wrap all that into a "DoResearch" type function.

Ex:
Code: [Select]
// Note I'm using some fake/nonexistent function names in here
lab.DoResearch(int techID)
{
  int availableScientists, maxAssignableScientists, numScientists;

  // Get available scientists
  availableScientists = Player[lab.ownerNum].GetAvailableScientists();
  // Get max scientists for research
  maxAssignableScientists = Technology.GetMaxScientists(techNum);

  numScientists = min(availableScientists, maxAssignableScientists)

  lab.Research(techID, numScientists);
}


My next suggestion is to get rid of the cascaded if statements all together. Use an array of some kind, and a global counter variable that tells you how far into the list you've researched. (Put it in a global struct that gets saved by GetSaveRegions). Then, use the same function as the callback for all research being completed. Each time it's called, it increments the index of the research being completed, and if it's not passed the end of the list, it'll start the next research.

Ex:
Code: [Select]
// Global AI research order array
// Note: I STRONGLY suggest using symbolic names here,
//   such as those listed in OP2Helper\EnumTechID.h,
//   or something equivalent for tech files that you wrote.
//   (But, here is a numeric exmaple instead)
int AIResearchList[] = {
  2701,  
  2702,
  2703,
  ...
};

// Trigger callback function
SCRIPT_API void AIResearchCallback()
{
  if (scriptGlobal.AI.researchIndex < SizeOfResearchList)
  {
    lab.DoResearch(AIResearchList[scriptGlobal.AI.researchIndex]);
    scriptGlobal.AI.researchIndex++;
  }
}

Ideally however, you should make sure the DoResearch was actually successful before incrementing the researchIndex. If no scientists were available or the lab is destroyed or disabled or whatever, then the AI will stop researching. You may need to design something to jump start research again if it ever stops. Remember that just because you order a lab to start researching something, it doesn't mean it will necessarily finish. Prime example is if the player destroys their lab. In which case the research callback will never get called to start the AI on it's next research, so all the research will just hang.

Also, you'll need to start off the first research somewhere outside of the callback. (You should probably also initialize the researchIndex to 1, and have the initialization research element 0 in the list).


As for the crash, I suspect maybe the enumerator failed to find the lab. I have no idea what sort of error checking is done on the lab functions from that library you're using. It might just assume that it's always passed a valid lab. If it's not, then it could probably crash the game. There are two possible problems I see with your code for finding the lab. The first one is, it doesn't loop, so if it finds another unit first, then it won't check again for the lab. This could happen if the rect you're using to check in is too big, and another unit moves into that rect. The other problem, is there is no error reporting if it doesn't find a lab. It could be that your rect is wrong, or the lab is destroyed, and no unit at all is ever found. In which case the if is never executed, and your bLab variable is never properly initialized.
Title: Multiple Questions Asked
Post by: Hidiot on October 19, 2007, 10:49:51 AM
Well, the code I use to find the basic lab is the same I used for the Vehicle Factory, and that one worked.

The rect is pretty much set to cover the whole building and adjunct tiles, as I don't know what it looks for when spotting a building.


If I'm not dead by tonight, I'll try to work on those improvements you suggested. Me... I'd just be happy if the damn thing would work :/
Title: Multiple Questions Asked
Post by: Hooman on October 20, 2007, 04:35:32 AM
Why not just use a building enum? It'd probably be much easier. Then you can be sure it only returns labs of the given type owned by a certain player. The only thing you really need to check then, is if the lab is busy/building/dismantling. Maybe check the location as well if you have more than one lab of the same type for the AI.
 
Title: Multiple Questions Asked
Post by: Hidiot on October 20, 2007, 12:48:54 PM
hmmm... I might check a few things tonight, when everything is over... Couldn't work last night, cause I was nearly dead (really)

Whilst reading and understanding what you said, I can say:

1. the basic lab SHOULD be taken the right way. It checks if the variable basiclab is mapBasicLab.

2.

Code: [Select]
    if (Player[1].Scientists()<5) if (Player[1].HasTechnology(2701)==0) bLab.Research(2701,Player[1].Scientists()); else;
       else if (Player[1].HasTechnology(2701)==0) bLab.Research(2701,5);

It might be my fault for showing all the code there, but if we take one example, like for the first tech, I made it with the followoing in mind: the SCRIPT_API void Basicres() contains all the researches in the basic lab, and the trigger that calls it is a tiime trigger that runs every 4 marks, to ensure a restart of a research in case the lab has been destroyed (gonna go do a few things, to make the trigger stop in case the lab was destroyed and restart after it hs been rebuilt. that might cause a problem or 2 later on). Then I used that jumbled up if soup to ensure all conditions are met (for when I will eliminate the Player[1].GoAI; I'll do a funtion that always gets the number of scientists assigned, by getting the number of buildings that use scientists, get the number of used scientists, subtract them from Player[1].Scientists() and get the number of free scientists. For now, I am 100% sure the AI has over 255 scientists though, and that is surely enough. Then it also checks if the AI has over 5 scientists. If it does have over 5 scientists, it will assign exactly 5 scientists to the research.

EDIT: I've found a way to make the nest look nicer:
Code: [Select]
    if (Player[1].HasTechnology(2701)==0) if (Player[1].Scientists()<5) bLab.Research(2701,Player[1].Scientists());
                                          else bLab.Research(2701,5);

The structure I use is to se if I can get ti to work. Efficiency in the first run isn't my goal. To finish first, first you must finish... And I only want to see this code soup work! Then I'll make it work fast and efficient.
Title: Multiple Questions Asked
Post by: Hooman on October 21, 2007, 04:48:31 AM
It wasn't so much a question of fast or efficient. Just easy to write and maintain. The main reason for writing it like I suggested, is it's easy to get it right. Plus, it's easy to edit without introducing bugs when you change things. It also tends to save typing, which makes it a little more pleasant to write in the first place.


As for the possible bug in your code....
Code: [Select]
 if(Rectenum.GetNext(basiclab)==1)
       {

right there. If it doesn't find anything, it returns 0, and then your check for mapBasicLab is never run. Or any of the other code either, such as the assignment to bLab from basiclab.
 
Title: Multiple Questions Asked
Post by: Hooman on October 21, 2007, 08:09:45 AM
Had a few thoughts.

First, the time trigger would work. You might want to try to find the lab in the time trigger itself rather than use a stored value. You would have to check that a stored value is still valid before using it anyways, so why not just find it while your at it. Or, probably a little better efficiency wise, is to store it once you've found it, and use the stored value for as long as it's valid, and if not, then search for a new lab unit. If one can't be found, then abort the rest of the trigger. (Return).

In the code I posted, I was just thinking I never setup a ResearchTrigger at the end of my procedure. I should probably have done that when I told the lab to start researching. That would of course make sure the research callback keeps getting called whenever research is done. This is possibly more efficient than a time trigger, but not really with the way triggers are implemented by OP2. It was fairly natural as long as research doesn't get interrupted, although, the time trigger way can auto recover from research interruptions. Might as well use a time trigger.

Also, don't bother trying to turn the trigger on and off. The amount of processing time saved by turning the time trigger off is negligible. Trying to pause and restart it with a complex system of triggers will likely turn out to be much less efficient. Just try to keep your callback small, and have short circuiting returns when no work needs to be done. You don't want to be spending a lot of time in the callback if it's not going to do anything that tick.


The game already calculated the number of available scientists, as well as the number of available workers. Maybe consider using the ones the game already calculates for you. I suppose the game might not bother calculating them for a player with GoAI() set for them, but I haven't really checked. Anyways, the offsets of the available number of workers and scientists in the Player record are 0xA4 and 0xA8. The player records are 3108 bytes long. So, to get the available number of scientists you might do something like this:

Code: [Select]
numAvailableScientists = *(int*)(0x56EF1C + 3108 * playerNum + 0xA8);

 
Title: Multiple Questions Asked
Post by: Hidiot on October 21, 2007, 09:41:17 AM
Hmm... The code I use to find the lab is the exact same one that Mcshay used in his working AI demo to find and use the Vehicle Factory.
I just turn the trigger off to avoid any possible stupid problems that might occur.

And I'm having a hard time understanding that callback Need mor etime to ponder upon it.

And uh... The numAvailableScientists = *(int*)(0x56EF1C + 3108 * playerNum + 0xA8); thing could work also... as long as I don't mess something up :)
Title: Multiple Questions Asked
Post by: Hidiot on November 03, 2007, 01:19:00 PM
ok... I've tried many things, but nothing works.


Final stage of the code is as follows:

First, the code that finds the lab and calls the research script every mark. The code finds the lab the way it should. That means bLab is actually a basic lab.
Code: [Select]
        
if(!bLab.IsLive())
    {
        IUnit lab;
        InRectEnumerator Rectenumbl(MAP_RECT(51+31,30-1,55+31,34-1));
        if(Rectenumbl.GetNext(lab)==1)
        {
            if (lab.GetType() == mapBasicLab)
            {
                switch(lab.GetBusy())
                {
                    case ctMoDevelop:
                    case ctMoUnDevelop:
                    case ctMoDismantle:
                    case ctMoResearch:
                        break;
                    default:
                    bLab = lab;
                    Trigger &basre = CreateTimeTrigger(1,0,100,"Basre");
                    break;
                }
            }
        }
    }

And here's where it all goes wrong:
I am using that "re" out of desperation, to make sure the research isn't called 1000 times in one millisecond.
int nAS = *(int*)(0x56EF1C + 3108 * 0 + 0xA8); (Used to get the scientists... in case there are less than 5).
Code: [Select]
    if (!Player[0].HasTechnology(2701) && re==0) {if (nAS<5) {bLab.Research(2701,nAS); re=1;}
                                        else {bLab.Research(2701,1); re=1;}}
Title: Multiple Questions Asked
Post by: Hooman on November 07, 2007, 06:19:56 AM
How do you know for sure? Did you try adding some sort of output display? Is bLab really a handle to the lab?

Did you try to comment out the if block and see if it still crashes?

If the if block is causing the crash, maybe comment it out and put in a single bLab.DoResearch call with hardcoded parameters and see what happens. Play with the parameters a bit and see if out of bounds values might be causing a crash.

Maybe change the ID on bLab and see what happens. It will probably crash, but does it crash in the same manner as it's currently crashing?



Also, try putting a supernova next to the lab, and level it. See what happens then. Provided you got things to not crash up to that point of course.
 
Title: Multiple Questions Asked
Post by: Hidiot on November 07, 2007, 11:56:37 AM
Uh should have been mor especific (me):

I tried everything I could:
-added output display with if (bLab.GetType()==mapBasicLab) and it returns true.
-commented it out and it stopped crashing
-changed the Id on the lab, same thing
-always crashes when it gets to the research instruction. Tried that with a time trigger.

havn't tried putting in other parameters... but if the TechID param (which is shortint in the IUnits.h) isn't the one you find the ResearchEnums, then, what makes it work?

I've been on it for more than a week and I'm rather fed up with it... I am 100% sure the bLab.Research(2701,1) is crashing. I put in just one scientists, to make sure that isn't the problem, and no, it isn't.
Title: Multiple Questions Asked
Post by: Hooman on November 08, 2007, 01:23:46 AM
Ahaha. You might want to check that assumption right there. The internal command packets use techNum, where the first item in the tech file gets 1 (I think 0 is a dummy tech, but maybe the first is 0). Does the function you're using take in techNums or techIDs? There is an internal function that translates techIDs to techNums. Maybe try using low numbers and see what happens. It might be easier to see if you're doing this for a lab that the player owns instead of the AI. Then you can see what research pops up in it. Or you can actually check the source of the function you're using and see if it does any translation. (Is the source even provided or just a header file? Well, the parameter name should at least be suggestive of what type it expects.)
Title: Multiple Questions Asked
Post by: Hidiot on November 08, 2007, 02:05:29 AM
The header is the IUnit.h one, with the attached commands.h  Didn't find anything much in there. No source of a working example that I can study sadly.
Code: [Select]
void Research(short techID,short numScientists);
Um... if you should happen to find what that internal convertor is (assuming it also works for made-up techs added to th tech tree) before I do, please do tell!
Title: Multiple Questions Asked
Post by: Hooman on November 09, 2007, 08:50:35 PM
It's an internal function that's not exported. It just looks through the internal data structures for the techs for the one with the given techID and returns it's index. It'll of course work for made up tech files other than the originals. I wouldn't suggest you go looking for the function yourself. Not unless you're feeling really bold and want to add a bit of assembly to your project. Although, the relevant label can certainly be found in the posted .udd file for OllyDbg if for some reason you do decide to go that route.


Just test out a few small values for the techID parameter, like 1, 2, 3. See what happens. I'm actually not that familiar with IUnit. Plus it's been ages since I've really done any OP2 programming.
 
Title: Multiple Questions Asked
Post by: Hidiot on November 10, 2007, 09:42:23 AM
Uh... adding 1 gives me the "Intentionally left blank" research and adding 2 gives me the next research in numer order. meaning the TechID number 2004.


I'd like to know that internal function, even if it's ugly... I do not want to loose all my hair looking for each value I need or re-writing the tech file so the techs come in correct numerical order.
Title: Multiple Questions Asked
Post by: Hooman on November 10, 2007, 02:38:45 PM
Ok, looks like that is the problem then.

Although, one of the reasons why I said you probably shouldn't bother is that this is better handled in library code, such as an edit to the function your calling. But then I don't see Eddy around much anymore, so I doubt an update will be too forth comming.


Here's the relevant internal function:
Code: [Select]
00472D90 >/$  83EC 04       SUB ESP,4                              ;  Function: Research.GetTechNum(int techID) (returns techNum or -1)

The hidden "this" pointer should take a value of: 0x56C230
As in, that's the address of the Research object.


A typical calling sequence might look something like this:
Code: [Select]
int __declspec(naked) GetTechNum(int techID)
{
  __asm
  {
    MOV EAX, [techID]
    MOV EDX, 0x472D90
    PUSH EAX
    MOV ECX, 0x56C230
    CALL [EDX]
    RETN
  }
}

Note: I might have the __declspec in the wrong place. If the compiler complains, move it before the return type (before the "int").

You can probably guess I didn't test any of that out, so don't be too surprised if it crashes or something.



Oh, and if that techID can't be found, it'll return -1. You might want to add some error checking in your code to check for this when you use that function. If it's not -1 then it should be a valid index of a tech. Although you can probably ignore this initially when testing, but it's a bit of a bad habit. Especially since an edit to your tech file could probably crash the game if you're not checking for that.
 
Title: Multiple Questions Asked
Post by: Hidiot on November 18, 2007, 07:23:29 AM
The game crashes...

I tried understanding that piece of code, but I'm a bit lost...

I can't understand exactly what I'm supposed to fit in my dll and I definitely can't understand what those commands do. I tried understanding them, but I got lost... maybe if I understood more, I would have a better chance at spotting potential problems :P
Title: Multiple Questions Asked
Post by: Hooman on November 18, 2007, 02:33:18 PM

Heh, the code I posted had 3 bugs in it. Can you spot them all? :P

Code: [Select]
int __declspec(naked) GetTechNum(int techID)
{
 __asm
 {
   MOV EAX, [ESP + 4]
   MOV EDX, 0x472D90
   PUSH EAX
   MOV ECX, 0x56C230
   CALL EDX
   RETN 4
 }
}

First of all, I should maybe trust the compiler a little less. With "naked" it assumes you setup your own stack frame. Not a problem, since a function this simple doesn't really need much of one. Except when I tried to load the parameter into a register, it was referenced off of EBP instead of ESP. It just assumed I'd setup EBP. Wasn't really expecting that since use of EBP for local variable access usually gets optimized away, particularly for the majority of the code I've read, but I guess using it is the default.

The next big problem was the [EDX], which was dereferencing the address of the function before calling, when it should have been calling direct. Basically it read the first few bytes of the correct function and interpreted them as the address of where it should call, then it went off to try and execute some invalid area of memory. Hence the crash.

The third problem was the RET didn't pop the parameters off the stack, thus leaving you with a corrupted stack when you returned.



Well, I tested out this version, and it works now.


Oh, btw, the techs get sorted on their techID. So the techNum won't be the order it appears in the file, but rather the sorted order of the techID for that tech. So if you checked for techAgridome (using the enum from OP2Helper, or tech 2101), it should return 2, even though that tech is listed somewhere in the middle of the file. The techID for the Spider is 2099, and it's techNum was 1.
 
Title: Multiple Questions Asked
Post by: Hidiot on November 19, 2007, 02:49:08 AM
Sad thing is... it still crashes :blink:  
Title: Multiple Questions Asked
Post by: Hooman on November 19, 2007, 06:33:56 PM
Hmm, is it different code causing the crash?

Are you checking for a return value of -1 in case it couldn't find a tech with the given tech ID?


Maybe try to seperate out the code for this section into a smaller project, and if it crashes then post the code for the smaller project. (Also, try to remove the excessive "teaching" comments that we put before each function in the template).
 
Title: Multiple Questions Asked
Post by: Hidiot on July 16, 2008, 10:45:54 AM
I decided to try again at working on this thing, but the new compiler keeps giving me headaches.

Like the following:
Main.obj||error LNK2019: unresolved external symbol "public: void __thiscall IUnit::Research(short,short)" (?Research@IUnit@@QAEXFF@Z) referenced in function _Basre|
Does the same for all IUnit.h functions.

I can't figure out what it wants right now...
Title: Multiple Questions Asked
Post by: Hooman on July 16, 2008, 10:16:08 PM
To use IUnit.h, you also need to include the .lib file. The .h just describes the classes, but doesn't have any of the code to implement the functions. These functions are NOT exported from Outpost2.exe. They were custom built. You need to include the .lib file that Eddy-B supplied with the header, so that the linker can find the function code.
 
Title: Multiple Questions Asked
Post by: Hidiot on July 17, 2008, 04:37:56 AM
:huh:  d'ooh!  :wacko:  :oops:


Now why didn't I think about checking the libraries, not just the headers... Probably that headache had something to do...

Thank you Hooman!


Now I'll probably get back to that annoying researching.


I think I've found a thing that causes a crash:
Code: [Select]
int __declspec(naked) GetTechNum(int techID)
{
__asm
{
  MOV EAX, [ESP + 4]
  MOV EDX, 0x472D90
  PUSH EAX
  MOV ECX, 0x56C230
  CALL EDX
  RETN 4
}
}
With this call: GetTechNum(2701);

I've stripped everything but this in the trigger and the game crashes upon executing. Removing this last command stops the crashes.
Title: Multiple Questions Asked
Post by: Hooman on July 17, 2008, 08:03:56 AM
Yeah, it helps to check what compiler stage errors occur at. Compile time and link time errors tend to be quite different. Link errors are usually caused by a mission .lib file, or forgetting to implement a function that was declared and called.


That function you posted looks correct. I just verified the address constants. I also checked the function prototype, and the calling convention also seems correct.

What if that techID isn't found? I think the function returns -1 in that case. Would a value of -1 potentially cause your code to crash?
 
Title: Multiple Questions Asked
Post by: Hidiot on July 17, 2008, 08:36:25 AM
I don't really know what to say...

Quote
BEGIN_TECH "Astronomy" 02701
    CATEGORY        1
[...]
    COST            400
    MAX_SCIENTISTS  5
    LAB             1
END_TECH

Doesn't the function take the TechID as it's present in the tech file?

I put the function on its own to test, not using its result anywhere, so I don't know why it would crash because of the result...

If it won't work, I'll have to take the time and write a silly C++ program or something for turning a tech file into a sorted Technum list to work with... I just really wanted the dll to do the conversion.

Just out of curiosity, does the function return 0 or 1 for the lowest TechID value?


Hooman, since I know you look around here often enough, I'd like to ask if there's any way the Residence demand could stop affecting morale until it's researched, just like Universities, Nurseries, etc.?

EDIT: Oh, would just like to mention the following:
When using Records for buildings, even if you start making a kit or have one in the SF bay (probably needs to be bay 1, I suppose) the record will use that building and not make a new one. I know I asked something about this before in this thread.
Title: Multiple Questions Asked
Post by: Hooman on July 17, 2008, 02:39:29 PM
Quote
Just out of curiosity, does the function return 0 or 1 for the lowest TechID value?

It's a 0-based index. But, the entry at index 0 might be a dummy tech that you never research, and isn't loaded from the file.


Quote
I'd like to ask if there's any way the Residence demand could stop affecting morale until it's researched
I don't believe so. Not any easy way at least. I think you'd need to overwrite some code to do that.

Quote
When using Records for buildings, even if you start making a kit or have one in the SF bay (probably needs to be bay 1, I suppose) the record will use that building and not make a new one. I know I asked something about this before in this thread.
Say what? Do you mean the UnitRecord in the SDK? (Exported by Outpost2.exe). Or do you mean the structs used by BaseBuilder.h? What exactly is the problem? I'm not too sure what you're saying here.



As for the crash, if you give me a crash address then I can tell you a lot more about it. Having the value of EIP would be very useful here. If Windows doesn't tell you a crash address, then attach with a debugger (OllyDbg can do this), or check the Windows error report, and try to find EIP in the register dump.
 
Title: Multiple Questions Asked
Post by: Hidiot on July 17, 2008, 03:28:41 PM
I meant when using RecordBuilding, the exported function... the name didn't stick to my mind at that point. And I just wanted to throw that in there so as to not forget about it, as it's pretty important for speeding the AI up a notch. It's not a problem to be solved :)

Here's what I could find by checking the Technical info on the crash:
Exception Information
Code: 0x0000005
Flags: 0x00000000
Record: 0x0000000000000000
Address: 0x0000000000000000
There's no mistake or typo, all these last 3 things have only 0's

Next comes the loaded Modules, Thread(s) with lots of hex info.

Did I get anything good or need I search further?
Title: Multiple Questions Asked
Post by: Hooman on July 18, 2008, 12:47:46 AM
No, that crash info is pretty useless I'm afraid. Although, you can get a crash address of 0 if you make an indirect call through a null pointer. It doesn't crash until after the EIP has been updated in that case. Not exactly very useful for debugging.

If you have OllyDbg installed, then attaching to the crashed program can provide a lot of info. If you do get a crash address of 0, then the information on the stack will be very useful. Particularly return addresses. That's pretty much the only way to find out where the last call came from. Keep in mind that not all things marked as return addresses on OllyDbg actually are return addresses. It basically guesses, depending on whether or not the value on the stack is a potentially valid address in a code section.


The Thread section contains a register dump. That might be useful. I just forced a crash, and I noticed the EIP value listed there doesn't match the address given above.

At any rate, the crash address will only really be useful if it starts with 4 or 5 (since that range would put it in Outpost2.exe), or maybe 11 or so (which would put it in the DLL), but I don't have your code so I couldn't check into a crash in your DLL. If the crash address is 0, then try to find a similar value on the stack. Something with 6 non-zero digits, where the first digit is 4 or 5.
 
Title: Multiple Questions Asked
Post by: Hidiot on July 18, 2008, 04:18:43 AM
How's about... I attach the source,map file and techtree (to avoid complications)?


Sorry, but I don't think I can get it right myself...
Title: Multiple Questions Asked
Post by: Hooman on July 18, 2008, 01:01:54 PM
Hmm, this caught my eye (near the very end):
Code: [Select]
    if(Rectenum.GetNext(RobS)==1)
You could have just written:
Code: [Select]
    if(Rectenum.GetNext(RobS))

In fact what you have written is bad for one simple reason. The return value is an int, but it's interpretation is as a boolean value. In that situation, any non-zero value should evaluate as true. But if you compare (value == 1), then you've change the truth value. By writing that, you make all non-1 values false. In other words, you've altered the truth values for all integers other than 0 or 1. It's good practices to never compare a boolean value against 1. (If you have to, use != 0).


You're using global variables here:
Code: [Select]
IUnit x,cmine,csmelter,structf,structf2,vechiclef1,vehiclef2,Truck1,Truck2,Convec,Earthw,Robos,Robom,bLab;
Unit ConVec,Earthy,Dozer,RM,RS,Truc,Truckf;
int i,a,a1,a2,lm,lms,lq,ls,lv,re,metnum;
int nAS = *(int*)(0x56EF1C + 3108 * 0 + 0xA8);
//   int nAS = *(int*)(0x56EF1C + 3108 * 1 + 0xA8);

Two things bad about this. First, is that some of these variables probably need to be saved and restored when the player Saves the game and Loads it again. To do this you need to place them in a struct, and then declare an instance of the struct.

Code: [Select]
struct ScriptGlobals
{
IUnit x,cmine,csmelter,structf,structf2,vechiclef1,vehiclef2,Truck1,Truck2,Convec,Earthw,Robos,Robom,bLab;
Unit ConVec,Earthy,Dozer,RM,RS,Truc,Truckf;
int i,a,a1,a2,lm,lms,lq,ls,lv,re,metnum;
};

// Declare instance of struct
ScriptGlobals scriptGlobals;

Then update GetSaveRegions to use the new struct variable.
Code: [Select]
void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = &scriptGlobals; // Pointer to a buffer that needs to be saved
bufDesc.length = sizeof(scriptGlobals);   // sizeof(buffer)
}
Oh, and btw, you have the bufDesc.length set to 8000 for no real reason.

The other thing that was odd about your globals, was the initialized one.
Code: [Select]
int nAS = *(int*)(0x56EF1C + 3108 * 0 + 0xA8);
This will, when the DLL is loaded into the process address space of OP2, try to read Player[0].numAvailableScientists. This would happen before InitProc, and quite possibly before that Player object is even initialized. Plus, there's no reason to cache that value as it will change over time. This initialization is really silly. I wouldn't even declare the variable here, since for it to be useful, you have to read the value just before you use it. In that case, if you're going to cache the value, do it in a local variable, not a global one.


Oh wow. This is actually kind of funny:
Code: [Select]
for (metnum=0;metnum<=6+TethysGame::GetRand(6);metnum++)
What this will do, is generate a new random number each time through the loop, and then check if it should stop looping. This will actually decrease the expected number of meteors as the distribution is no longer uniform. For instance, you do not have a 1/6 chance of getting 12 meteors. You will only get 12 meteors if the random number generator produces a 5 the last time, at least a 4 before that, at least a 3 before that, .... Also, for the first 7 loop iterations, it will produce random numbers that don't do anything.

I think what you might have intended, is this:
Code: [Select]
int numMeteors = 6 + TethysGame::GetRand(7);
for (metnum=0; metnum < numMeteors; metnum++)
This will produce a uniformly distributed number of meteors from 6 to 12. Note that I changed the "<=" to a "<" and "GetRand(6)" to "GetRand(7)". If I hadn't changed these, then it would have produced from 7 to 12 meteors. Remember that the argument to GetRand is the range of numbers produced, and they will be from 0 to (range - 1), so GetRand(6) would produce numbers 0..5. Also, if you'd like to count out a specified number of loop iterations, the usual idiom is to use "<", and start counting at 0. (Instead of using "<=", and startnig the count at 1).

You have more global variables, seperated from the rest. Dump these in the struct.
Code: [Select]
//Groups
MiningGroup com1;
MiningGroup com2;
MiningGroup com3;
// ...


Code: [Select]
SCRIPT_API void StLab()
... the patron saint of research


You have some odd abbreviations and such. You also have a bunch of strings/function names in all lower case instead of mixed case. It makes it a little harder to parse the words and read. Plus, functions are usually verbs, and variables are usually nouns. You also use rather inconsistent indentation. (Or is that because of a different editor?) Oh, and this makes me cringe. It looks like it could actually be a typo:
Code: [Select]
SCRIPT_API void Nurseri()



Gah, and another project that uses IUnit. I don't have this library, so I can't compile these projects as is. But, after some editing, I do indeed find some odd behavior when that function is called. It doesn't crash, but it does cause empty log messages to be added instead of some text string. Hmm, I'll have to look into this further.
 
Title: Multiple Questions Asked
Post by: Hidiot on July 18, 2008, 01:13:47 PM
Eh, I know I'm very disorganized and do silly... very silly stuff.

I'll look at using cases from now on and will also try to tidy up that code. The names I place for variables or triggers are deliberate. If it's a typo it is most probably not used anywhere else.


Oh, and don't forget, I already warned people about the following in the first post:
Quote
Warning: Content might cause head aches, severe migraines or a possible stroke to any experienced map coder!
Title: Multiple Questions Asked
Post by: Hooman on July 18, 2008, 01:45:53 PM
Ahh, got it. The calling convention was wrong. The function was written so the callee pops the arguments off the stack (stdcall), but the compiler assumed the caller pops the arguments off the stack (cdecl).

I suppose I should have realized that. The default is cdecl, but class member functions tend to use stdcall, as do many other functions, such as the Windows API, and most of the functions in Outpost2. The functions in Outpost2 that are cdecl seem to be mostly C standard library functions. (C, not C++).


Code: [Select]
int __declspec(naked) __stdcall GetTechNum(int techID)
{
__asm
{
  MOV EAX, [ESP + 4]
  MOV EDX, 0x472D90
  PUSH EAX
  MOV ECX, 0x56C230
  CALL EDX
  RETN 4
}
}


What was happening, is that they were both popping the argument off the stack (the calling function, and the called function). Since too much got popped off the stack, values were lost and the stack got corrupted/offset. When that happens it usually crashes when it tries to return, since the return address isn't where it expects it to be.
 
Title: Multiple Questions Asked
Post by: Hidiot on July 18, 2008, 02:40:11 PM
Declaring all those variables inside the struct makes them unrecognizable for the rest of the file.

Also, the research works fine now. Finally, I can get on with this :)

Hooman... I am promoting you to the rank of Coding God in my list :D
Title: Multiple Questions Asked
Post by: BlackBox on July 18, 2008, 04:43:27 PM
Quote
Declaring all those variables inside the struct makes them unrecognizable for the rest of the file.
Easy fix. Just reference them with structVar.memberVar.

i.e. if you have
Code: [Select]
struct ScriptGlobals
{
int something;
};

// Declare instance of struct
ScriptGlobals scriptGlobals;

and you were previously just accessing it as 'something', change that to:
Code: [Select]
scriptGlobals.something
Title: Multiple Questions Asked
Post by: Hooman on July 18, 2008, 04:45:48 PM
You'll have to prefic the variables with the struct variable name.

Code: [Select]
ScriptGlobal scriptGlobal;
...
scriptGlobal.roboSurveyor.DoMove(...)


Edit: typo.
Seems BlackBox beat me to this.
 
Title: Multiple Questions Asked
Post by: Hidiot on July 18, 2008, 05:34:54 PM
Ok, I'm done for today...  guess I should have studied more by myself. Another black dot on the board for school <_<

Now on my to-do list:
Making a research list for a more streamlined research pattern.
Finding out how to keep the lab researching, as I've stumbled on something that's bugging me: The 2708 and after researches refuse to start, whilst researches 2701 to 2705 and 2707 research like they should, despite the exact same code used for all of them. In some versions of the dll, 2707 would be the one where the researching stops, despite no modifications to the research. The .GetBusy() I'm using to verify the state of the lab to put it to work if it's idling comes to mind.


EDIT: It seems very odd, but all researching seems to stop after mark 100...
Title: Multiple Questions Asked
Post by: Hooman on July 19, 2008, 07:37:23 AM
Hmm, that is odd. I might take another look at it. Is this an existing problem, or a new problem?


Well, you started me rambling on some ideas I had about research. It got kind of long, so I decided to start a new thread. (Plus it seems to be of fairly general interest, and not specifically related to your level). You can read it here:
http://forum.outpost2.net/index.php?showto...indpost&p=64728 (http://forum.outpost2.net/index.php?showtopic=4279&view=findpost&p=64728)
 
Title: Multiple Questions Asked
Post by: Hidiot on July 19, 2008, 10:15:16 AM
It's probably a new problem, as I couldn't get to mark 100 before with automated research anyway. I'll upload the current source and tech tree now, as I believe you still have the .map file (edited the tech file a bit today)


Hmmm... Is there a limit to the number of times a function can be accessed via the AIProc?
Title: Multiple Questions Asked
Post by: Hooman on July 21, 2008, 06:42:39 PM
Ok, the problem was traced back to running out of triggers. (Actually, it's more like running out of ScStubs, which Trigger derives from). In AIProc, it detected when the lab was free, and setup a time trigger that would start the next research in 50 ticks. During those 50 ticks, 13 time triggers were created, one for each time AIProc was called before the lab got put to use 50 ticks later by the first time trigger to execute. For each time trigger, two ScStub derived classes are created. If I remember right, one was the time trigger, and the other was a stub that handled calling the callback function in the DLL. This used up 26 ScStub entries in an array that can support a maximum of about 255. When each of those time triggers fired, it basically calls Disable(), since they were not set to repeat. They did NOT call Destroy(). If Destroy is not called, those ScStub entries are never freed, and eventually the table fills up, and you can't create anymore. Any attempt to create more just returns a Trigger stub with ID 255, which you can't do anything useful with. When that happens, Time Triggers are no longer created, and your callback that starts the next research stops being called.

If you use a Time Trigger for a one-shot event, you should probably keep track of the Trigger reference by storing it in that ScriptGlobal struct, and in the callback that it causes to fire, call trigger.Destroy(). Mind you, you should still avoid creating all those extra triggers meanwhile to start with. If you're ever low on triggers, creating that many at once could cause problems. At any rate, you'll still need to call Destroy to prevent eventual problems.

Alternatively, you can use a repeating time trigger, since in this case you actually do want to reuse the trigger, and have it repeat it's action. Then you don't need to Destroy it, and then Create it again. What you can do is setup the trigger in InitProc, or similar, and have it set to repeat. Then, in the start of the callback, you can check if the lab is busy, and if it is, just return from the callback without doing anything. There would be one slight difference this way though. Instead of starting the next research topic 50 ticks after the last one finishes, it will start the next topic on a multiple of 50 ticks after the last one finishes. This might be right away, or it might be up to 50 ticks later. It will in general not be exactly 50 ticks though.

Another solution for your situation, is don't change any code except where it creates the Time Trigger, and instead of delaying the call to your function by 50 ticks using the trigger, just call the function directly, and have it execute right away. As soon as one tech finishes, it will immediately start the next one.

Code: [Select]
  if (!scriptGlobals.bLab.GetBusy() && !Player[0].HasTechnology(3004))
  {
   //Trigger &BasicResearchTrigger = CreateTimeTrigger(1,1,50,"BasRe");
   BasRe();
  }

You might also consider using Research Triggers. Those will fire exactly when a research topic completes. If you still want to delay by exactly 50 ticks, then you can use both a Research Trigger and a Time Trigger. When the topic you are researching completes, the Research Trigger callback can start the Time Trigger. When the Time Trigger fires, it can start the next research topic.

Code: [Select]
ScriptGlobal
{
 // ...
 Trigger researchTrigger;
 Trigger researchTimeTrigger;
};

... (somehwere in InitProc)
scriptGlobal.researchTimeTrigger = CreateTimeTrigger(true, false, 50, "StartNextResearch");

... (new function)

SCRIPT_API void StartNextResearch()
{
 int bFailed = true;  // Default assumption
 // Determine next research topic
 int techID = ...;
 int techIndex = GetTechNum(techID)
 // Start the next research here ...

 // If failed, abort here without adjusting further settings
 // (Although, you can Destroy the research trigger if you want)
 if (bFailed)
  return;  // Will try again in 50 ticks

 // Disable the Time Trigger  (It is auto reset to a 50 tick delay, that starts when it's next enabled)
 scriptGlobal.researchTimeTrigger.Disable();
 // Set the Research Trigger
 if (scriptGlobal.researchTrigger.IsInitialized())  // Only destroy it if it's already in use
  scriptGlobal.researchTrigger.Destroy();  
 scriptGlobal.researchTrigger = CreateResearchTrigger(true, true, techID, playerNum, "ResearchComplete");
}

SCRIPT_API void ResearchComplete()
{
 // Start the next topic in 50 ticks
 scriptGlobal.researchTimeTrigger.Enable();
}


This will wait 50 ticks, and then start the first research. Whenever a topic completes, it will wait 50 ticks before starting the next. If it is unable to start, because the lab is destroyed, or disabled, then it will keep checking if it can start a new topic every 50 ticks. Note that the Time Trigger is never destroyed. It is only Disabled and Enabled. The Research Trigger needs to update which topic it is watching, and so you need to recreate this one, which means you need to Destroy the old one.


The next part is to simplify how you specify the list of topics to research. You'll want to put them in an array of some kind.
Code: [Select]
int researchTopicList[] = 
{
 2701,
 2702,
 // ...
};

You might also consider using an Enum, so the list is easier to read. Since you're designing your own tech file, you'd also have to write your own enum to do this.

Also, you might want to have two research lists if you AI can be either Eden or Plymouth. Remember that they can get different techs, or have different costs for researching each tech. It would then make sense to define two arrays if your AI needs to be able to play as either.
Code: [Select]
int edenResearchTopicList[] = 
{
 // ...
};

int plymouthResearchTopicList[] =
{
 // ...
};

Of course there is no need to stop there either. You can have more arrays if you want. Maybe you want an AI to have a random personality. Perhaps it's more war like one round, and another it's racing to launch a spacecraft, or just building up it's base.

Next you'll need a way of traversing the list. For this you can add another variable to the ScriptGlobal struct. One that keeps track of the research topic index.

Code: [Select]
ScriptGlobal
{
 // ...
 Trigger researchTrigger;
 Trigger researchTimeTrigger;
 int researchTopicIndex;
};

// In InitProc
scriptGlobal.researchTopicIndex = 0;  // Initialize to start of list

Then, each time you start a new research (successfully), you need to increment that value.

Code: [Select]
SCRIPT_API void StartNextResearch()
{
 int bFailed = true;  // Default assumption
 // Determine next research topic
 int techID = researchTopicList[scriptGlobal.researchTopicIndex];
 int techIndex = GetTechNum(techID)
 // Start the next research here
 if (techIndex != -1)
 {
  // Start the research (with as many scientists as possible)
  int maxScientists = GetMaxScientists(techIndex);
  int numAvailableScientists = GetNumAvailableScientists(playerNum);
  int numScientists = Min(maxScientists, numAvailableScientists);
  scriptGlobal.lab.Research(techIndex, numScientists);

  bFailed = false;
 }

 //... Rest of function, as above
}

SCRIPT_API void ResearchComplete()
{
 // Increment the topic index
 scriptGlobal.researchTopicIndex++;
 // Start the next topic in 50 ticks
 scriptGlobal.researchTimeTrigger.Enable();
}



What's still left to do, is terminating the research cycle once the list is complete, and determining the max number of scientists that you can assign to each research topic. There is a way to read this value from the in memory structs, so I'll provide a function to do that. Then you don't need to specify it in a seperate list, which is also error prone, especially if the tech file is updated.

Note: This following code to retrieve the max number of scientists you can assign to a research topic is untested.
Code: [Select]
int __declspec(naked) __stdcall GetMaxScientists(int techIndex)
{
 __asm
 {
  MOV EAX, [ESP + 4]
  MOV ECX, [(0x56C230+4)]  // Load TechInfo*[]*
  MOV EDX, [ECX + EAX * 4] // Load TechInfo*  (of techIndex)
  MOV EAX, [EDX + 0x14]  // EAX has return value of TechInfo.maxScientists
  RETN 4
 }
}


Now, you just have to make sure to terminate the research system when the list is complete. First, you'll need to know how big your list of topics is. There is an easy way to get the compiler to figure this out.
Code: [Select]
int researchTopicList[] = ...
int numResearchTopics = sizeof(researchTopicList)/sizeof(researchTopicList[0]);

Now, when the topic is incremented to this value, you need to shutdown the trigger system so it doesn't walk off the end of your list.
Code: [Select]
SCRIPT_API void StartNextResearch()
{
 if (scriptGlobal.researchTopicIndex >= numResearchTopics)
 {
  // Shutdown trigger system. All research has been completed.
  if (scriptGlobal.researchTrigger.IsInitialized())  // Need to check this in case the list was empty (and good habit)
   scriptGlobal.researchTrigger.Destroy();  // Don't need this anymore
  scriptGlobal.researchTimeTrigger.Destroy();  // Don't need this anymore either
  return;  // Don't try to do anything more
 }
 // ... Rest of function, as above
}

Note that I chose to destroy the trigger when it tried to start a new topic, after the old one finished, rather than immediate after the last topic started. This is because that last topic might not finish, if say the lab is destroyed. We want it to be restartable. This is also why I chose to increment the topic index after the research had been completed. If some topic doesn't complete, we don't want to skip over it and trying researching the one after it. Also, if the list of topics is empty, I want to system to just exit gracefully. Hence why I put the termination condition at the start of StartNextResearch, rather than after the increment in ResearchComplete.


There are a few problems that I'll leave you to solve. First, what happens when a lab is destroyed? How will you restart things and continue with the research? (I built the code so that this will hopefully be easy to do). You also need to figure out how the system will work with the 3 different lab types. As I wrote it, it assumes a single lab type for all research topics. You could use 3 seperate lists, but that won't guarantee you don't start a research topic until you're finished the prerequisites. You could also check which lab type a topic needs (I can provide code to get this), and then see if the variable for that lab type is set. If it is, start the topic at that lab. That would make the research topics serialize nicely, and should prevent weird dependcy bugs, provided your list of topics is correct. It's probably a little less obvious how to do research in parallel this way though. See if you can find a solution.


Also, I'd like to include the following disclaimer. I haven't tried out any of this code. I can't even guarantee if it will compile. Try it and find out for yourself. I've also been editing it as I go along, so it might not even be consistent.
 
Title: Multiple Questions Asked
Post by: Hidiot on August 17, 2008, 01:44:37 PM
Ok, so I tried adding those things and ended up wit hthe following pretty interesting errors:

|28|error C2059: syntax error : '{'
|28|error C2334: unexpected token(s) preceding '{'; skipping apparent function body
|301|error C2039: 'BasicResearchLine' : is not a member of 'scriptGlobal'
|307|error C3861: 'Min': identifier not found, even with argument-dependent lookup

line 301 error is obvious, as it's related to the errors on line 28, which is the Basic ResearchLine declaration that goes like this:
   struct scriptGlobal
   {...;
    int BasicResearchLine [] =
/        {
        2701,
        2702,
        2703,
        2704,
        2705,
        2707,
        2708,
        2709,
        3004
        };
    ...;
    };

Line marked with "/" is line nr. 28

Nice of it to refuse the declaration of an array in that struct.

The error on line 307 is also pretty interesting...
Title: Multiple Questions Asked
Post by: Hooman on August 17, 2008, 06:08:53 PM
Don't declare the array in the struct.

First reason: It doesn't make sense to do so. The struct is only a data type, and not something that has memory at runtime. Hence, you can't assign to any of it's fields. Instead, you must create an instance of that struct, and assign to the fields of the struct instance. (A struct instance does have memory). This is the difference between:
Code: [Select]
struct ScriptGlobal
{
// ...
};
and
Code: [Select]
ScriptGlobal scriptGlobal;

Second reason: You don't want to do this anyways. You don't need to save constant values, so they don't need to be stored in the script global struct. Just place the array before or after the struct. What you do want to save in the global struct, is the current index into your array.


Min isn't a built in function. You'll either need to write it yourself, or find some standard library header to include that contains it. It's not very hard to write though.
 
Title: Multiple Questions Asked
Post by: Hidiot on August 18, 2008, 02:53:53 AM
Ok, fixed everything.

There is a crash and it comes from:
        int maxScientists = GetMaxScientists(techIndex);

The GetMaxScientists is the same one you gave me.

EDIT: there's a second crash spot I found: scriptGlobal.researchTimeTrigger.Disable();
Title: Multiple Questions Asked
Post by: Hooman on August 18, 2008, 06:05:36 AM
Hmm, try this for a replacement:
Code: [Select]
int __declspec(naked) __stdcall GetMaxScientists(int techIndex)
{
__asm
{
  MOV EAX, [ESP + 4]
  MOV ECX, 0x56C230+4   // Load &Research.TechInfo*[]*
  MOV ECX, [ECX]    // Load TechInfo*[]*
  MOV EDX, [ECX + EAX * 4] // Load TechInfo*  (of techIndex)
  MOV EAX, [EDX + 0x14]  // EAX has return value of TechInfo.maxScientists
  RETN 4
}
}

It seems that the inline assembly for:
Code: [Select]
MOV ECX, [0x56C234]
results in the same instruction as:
Code: [Select]
MOV ECX, 0x56C234

The second one clearly means to load a constant value into a register. The first form should, according to the rules I'm most familiar with, be a load from a memory address. Seems that's not the case. I'm most familiar with NASM style assembly. The C++ inline assembly seems to be based on MASM style assembly though. Mind you, I would have thought they would be the same for something like this.



As for the second case, some crash details would be helpful.
 
Title: Multiple Questions Asked
Post by: Hidiot on August 19, 2008, 03:31:32 AM
I just switched to a new computer yesterday and I found out I might need to re-format to add a partition on my only installed hard drive, thing that I don't even know how to do...

Bottom line, it'll be another few days until I actually get my coding back up again.
Title: Multiple Questions Asked
Post by: Hooman on August 23, 2008, 05:27:50 AM
I didn't look too closely, but I did see something you should fix that could cause a crash.

In AIProc, you have:
Code: [Select]
  Trigger &BasicResearchTrigger = CreateTimeTrigger(1,0,60,"StartNextResearch");

Remember that AIProc runs every 4 ticks. This will likely spam the engine with lots of identical triggers, and eventually you'll run out. When that happens, the game will crash. It might not crash here though. It might crash elsewhere if you try to create a trigger.

Same for the line in the following block:
Code: [Select]
  Trigger &StandardResearchTrigger = CreateTimeTrigger(1,1,50,"StdRe");


Hmm, actually, come to think of it, the line you identified as the cause of the crashes would likely only crash if that variable wasn't initialized, or if it was already destroyed. From the way the function is written, it's basically assuming that trigger was the one that caused entry, but in this case, it isn't. The "BasicResearchTrigger" temporary that you created in AIProc is the one causing entry. You should have stored the created trigger in the scriptGlobal struct.

Code: [Select]
scriptGlobal.researchTrigger = CreateTimeTrigger((1,0,60,"StartNextResearch");

Same with the other line.
 
Title: Multiple Questions Asked
Post by: Hidiot on September 11, 2008, 07:47:48 AM
I've been looking a bit and have noticed the lack of creation for the researchTimeTrigger.


The trigger shouldn't get called more then once(or a few times, due to the delay) each time the lab is found to be idling (not researching or being built or etc.)
I think I'll remove the time delay.
Is there any of immediately activating a SCRIPT_API without using a trigger?

Every time something related to the use of researchTimeTrigger tries to do something, it crashes... I think I might have misunderstood its use, so will look into it deeper to rename it or create it.


EDIT: Ok, I've fixed those crashes by realizing the time trigger was the trigger I used to initiate all research :P

Now I notice that the only thing that keeps my researches working is the initial call itself... re-enabling the trigger won't make the researches go on.

I can't do much else anymore now, so I'll leave the file for further inspection
Title: Multiple Questions Asked
Post by: Hooman on September 20, 2008, 05:48:25 PM
I made some changes and simplifications to your code in the SVN repository. I did it step by step, and commited after each step. You can check the log messages to see what was changed (TortoiseSVN -> Show log), and you can also see a Diff of each successive change (right-click on the revision -> compare with previous revision, double click a changed file to view (main.cpp)).

If you like the changes, then you should run "SVN Update" on your working copy. If you only want some of the changes, you can update to a specific revision (TortoiseSVN -> update to revision...). This also lets you backdate your working copy should you run "SVN Update" first, and then change your mind. The "View log" button will help you select a specific revision.


If you don't like any of the changes, then you can revert the changes (basically commit a previous revision as the new revision). Or if you've already made edits to your local copy, you may need to do a merge. In either case, either bug me on IRC for help, or read the Subversion book (or equivalently, the help file that comes with TortoiseSVN).


Edit: Oh, and I should mention that I only tested to make sure the file still compiles. I didn't check if it runs the same, or even if it runs at all.
Title: Multiple Questions Asked
Post by: Hidiot on September 21, 2008, 05:46:02 AM
All the changes are for the better so I used them.

Only problem upon compiling is the following:

Main.cpp|499|error C2679: binary '=' : no operator found which takes a right-hand operand of type 'Unit' (or there is no acceptable conversion)|

That's for this line:
scriptGlobal.bLab = GetFreeUnitAt(mapBasicLab, 51+31, 30-1, 55+31, 34-1);

I even changed the "unit" variable from GetFreeUnitAt to a IUnit type (cause bLab is IUnit also), but still nothing.


I've made a lot of trigger removals, though I suspect it might not have been wise. Those that were a one-fire will probably go back and I'll give handles stored in scriptGlobal so I can destroy them after usage, not just disable them.

The scripts that need to run repetitively will get trigger removals that I hope will stay. Might save some headaches with trigger destruction and re-enabling.
Title: Multiple Questions Asked
Post by: Hidiot on September 22, 2008, 07:37:17 AM
[size=8]Yay! for double posting![/size]

Now that we have the mighty svn, this topic will pretty much be a good place for me to leave a question I might forget later.

For instance: I need something to tell me the current power unused. If there's no direct way for it, then something to read the total power production and total power usage would do good.

I guess I could go check for every power generating building, then check for upgrades made on it, then check all the buildings what they are and how much they consume, but that'd be just too tedious and plain silly.
Title: Multiple Questions Asked
Post by: Hooman on September 22, 2008, 05:06:42 PM
Plus, those values are already calculated by the game. You can just read them with a mem hack pointer trick. You can find the details you need in the Forced Exports project headers, or even just use the project itself. It might be easier to just code up a pointer hack in one line of code though. The project needs NASM installed to build it.

Anyways, in that project, check Player.h, for both the base address of the player array, the size of the Player class, and the offset within the class of the field you want. The address would then be: base + size*playerNum + offset.