Update 6DEC: Added a version # to top of DisasterHelper.h. Now of version 1.0. See attached .zip file that contains .cpp/.h for newest code.
I finished a polished final draft of the DisasterHelper class. I'm happy to receive feedback on the mess, but since it is about 400 lines of code, I understand if people do not want to read through it. I'll be passing it off to Dave for use if he still wants it in his multiplayer rebalancing scenarios, so it would be easier change things now before that happens.
Due to limits on how functions are passed to Triggers, you cannot directly pass the function CreateRandomDisaster from DisasterHelper to an Outpost 2 Trigger function. However, you can simply wrap the function and then pass it without problem. See the initialization example below on what I mean.
If using this code in a single player scenario, you must ensure DisasterHelper is initialized after a game is re-loaded. This is best done by calling DisasterHelper.MapPropertiesSet(). If this is false, then you need to re-initialize it before calling. Not initializing the class before calling will not crash the game, it will just cause disasters to not be created when called for.
Debugging
In Visual Studio (or another IDE), if you leave the Solution Configuration in Debug mode instead of release mode, DisasterHelper will send warning messages via the in game chat console. In particular warnings about not initializing the class, not being able to find a LOCATION outside of a safe area, trying to create a vortex without first setting a VortexCorridor (MAP_RECT), or setting innapropriate duration/strength values are sent.
How to use it:
Below are three examples of how to integrate DisasterHelper into a scenario. They will all get disasters on the map in a reasonable manner. A lot more properties of DisasterHelper can be set if you want to massage them, such as disaster duration, disaster strength, percent chance of each disaster, etc. The default values should be noticeable without being too powerful.
Single Player (Colony Game) Concerns
If you use DisasterHelper in a colony game, you have to place your triggers in ScriptGlobal and do the rest of the requirements to ensure it is available for use between Saves/Loads of the scenario. When saving/loading the scenario, the class DisasterHelper will not retain its settings. The code below checks if DisasterHelper is not initialized every time CreateRandomDisaster is called to ensure the class data is restored after a load.
Since multiplayer scenarios cannot be saved/loaded, If you are designing a multiplayer scenario, you can disregard placing the trigger in ScriptGlobal and just initialize DisasterHelper in the function InitProc and not worry about checking if its properties are set later.
Example 1: Pairing DisasterHelper with BaseHelper in a multiplayer scenario. Safe Areas set Manually.
#include "DisasterHelper.h"
DisasterHelper disasterHelper;
//Shows setting up 4 base locations for a 4 player LoS game.
//Base starting locations are randomized.
int playerSlots[] = { 0, 1, 2, 3 };
const MAP_RECT baseSafeRects[] = {
{ 10 + X_, 10 + Y_, 30 + X_, 30 + Y_ },
{ 100 + X_, 10 + Y_, 30 + X_, 30 + Y_ },
{ 10 + X_, 100 + Y_, 30 + X_, 30 + Y_ },
{ 100 + X_, 100 + Y_, 30 + X_, 30 + Y_ },
};
Export int InitProc()
{
disasterHelper.SetMapProperties(256, 64, false); //MapWidth, MapHeight, Does map wrap East/West
RandomizeList(AutoSize(playerSlots));
for (int i = 0; i < TethysGame::NoPlayers(); i++)
{
// Create bases loop
CreateBase(i, startLocation[playerSlots[i]]); // See BaseHelper.h for documentation on creating startLocation array.
// Loop to create safe rects
disasterHelper.AddSafeRect(baseSafeRects[playerSlots[i]]);
}
CreateTimeTrigger(true, false, 1500, 3500, "CreateDisaster"); //Set time in ticks (marks / 100)
}
Export void CreateDisaster()
{
disasterHelper.CreateRandomDisaster();
}
Example 2: Auto find Command Centers and add MAP_RECTs of a pre-determined size to DisasterHelper. Designed for a use in a multiplayer scenario.
#include "DisasterHelper.h"
DisasterHelper disasterHelper;
Export int InitProc()
{
disasterHelper.SetMapProperties(256, 64, false); //MapWidth, MapHeight, Does map wrap East/West
AutoSetBaseSafeRects(30, 30);
CreateTimeTrigger(true, false, 1500, 3500, "CreateDisaster"); //Set time in ticks (marks / 100)
}
// Places a safeRectangle centered on all command centers currently in game.
void AutoSetBaseSafeRects(int safeAreaWidth, int safeAreaHeight)
{
for (int i = 0; i < TethysGame::NoPlayers(); i++)
{
PlayerBuildingEnum playerBuildingEnum(i, map_id::mapCommandCenter);
Unit unit;
while (playerBuildingEnum.GetNext(unit))
{
LOCATION loc = unit.Location();
MAP_RECT safeRect = MAP_RECT(loc.x - safeAreaWidth / 2, loc.y - safeAreaHeight / 2, loc.x + 30, loc.y + 30);
disasterHelper.AddSafeRect(safeRect);
}
}
}
Export void CreateDisaster()
{
disasterHelper.CreateRandomDisaster();
}
Example 3: Use DisasterHelper in a colony game scenario where dealing with Saving/Loading games must be taken into account.
#include "DisasterHelper.h"
DisasterHelper disasterHelper;
Export int InitProc()
{
scriptGlobal.DisasterTimeTrig = CreateTimeTrigger(true, false, 1500, 3500, "CreateDisaster"); //Set time in ticks (marks / 100)
}
Export void CreateDisaster()
{
if (!disasterHelper.MapPropertiesSet())
{
disasterHelper.SetMapProperties(256, 64, false); //MapWidth, MapHeight, Does map wrap East/West
disasterHelper.AddSafeRect(MAP_RECT(10 + X_, 10 + Y_, 30 + X_, 30 + Y_));
}
disasterHelper.CreateRandomDisaster();
}
The Actual Code
The header and CPP file for DisasterHelper are attached in a ZIP file. Beyond Outpost 2 they depend on vector, cmath and climits. You need a C++11 compliant compiler, but that should be pretty standard now (I think?). This code is currently contained in the repository at https://svn.outpostuniverse.org:8443/!/#outpost2/view/head/LevelsAndMods/trunk/Levels/RescueEscort. I'm using the scenario RescueEscort to test it, although this scenario is far from being completed I'm ensuring RescueEscort always compiles/runs fine when committing to it.
* Note: You may need to login with an Outpost Universe Forum Account to see the attachment.
Personally I would do away with the switch statement entirely and use a table approach. It's cleaner and easier to maintain without mistakes:
std::map<int, MAP_RECT> LOCATIONS_TABLE;
Export int InitProc()
{
initLocationTable();
/* Set time in ticks (marks / 100) */
scriptGlobal.DisasterTimeTrig = CreateTimeTrigger(true, false, 1500, 3500, "CreateDisaster");
}
void initLocationTable()
{
LOCATIONS_TABLE[0] = MAP_RECT(10 + X_, 10 + Y_, 30 + X_, 30 + Y_);
LOCATIONS_TABLE[1] = MAP_RECT(100 + X_, 10 + Y_, 30 + X_, 30 + Y_);
LOCATIONS_TABLE[2] = MAP_RECT(10 + X_, 100 + Y_, 30 + X_, 30 + Y_);
LOCATIONS_TABLE[3] = MAP_RECT(100 + X_, 100 + Y_, 30 + X_, 30 + Y_);
}
Export void CreateDisaster()
{
if (!disasterHelper.MapPropertiesSet())
{
/* MapWidth, MapHeight, Does map wrap East/West */
disasterHelper.SetMapProperties(256, 64, false);
SetDisasterSafeZones();
}
disasterHelper.CreateRandomDisaster();
}
void setDisasterSafeZones()
{
for (int i = 0; i < TethysGame::NoPlayers(); i++)
disasterHelper.AddSafeRect(LOCATIONS_TABLE[i]);
}
Note that the code syntax highlighter breaks on C++ style comments which is why I switched to C style comments instead and also on compiler directives (such as #include)... need to see if I can fix that.
Anyway, I dunno... I find table approaches cleaner and easier to work with than large control tatements. As you can see the setDisasterSafeZones() function is suddenly a lot easier to read but more importantly, to add an additional location you can simply add a new MAP_RECT entry to the LOCATIONS_TABLE.
The rest of it looks good. Really love the DisasterHelper object -- seems to be a great addition to the library of coding tools we have available to us! Might make sense to include it as part of the SDK... but we'll see when Arklon/BlackBox decide to move the way of Python. :P
Would still highly recommend using std::map vs. a c array for three reasons:
- Less likelihood of bugs, particularly memory issues.
- Easier to read/use.
[li]Since this is internal to the DLL and isn't being passed to the game at all, it makes sense to take advantage of well known libraries and tools.
[/li][/list]
When I think about it though a std::vector may even be a better option. Less overhead and access syntax is basically the same:
std::vector<MAP_RECT> LOCATIONS_TABLE;
/* ... snip ... */
void initLocationTable()
{
LOCATIONS_TABLE.push_back(MAP_RECT(10 + X_, 10 + Y_, 30 + X_, 30 + Y_));
LOCATIONS_TABLE.push_back(MAP_RECT(100 + X_, 10 + Y_, 30 + X_, 30 + Y_));
LOCATIONS_TABLE.push_back(MAP_RECT(10 + X_, 100 + Y_, 30 + X_, 30 + Y_));
LOCATIONS_TABLE.push_back(MAP_RECT(100 + X_, 100 + Y_, 30 + X_, 30 + Y_));
}
/* ... snip ... */
void setDisasterSafeZones()
{
for (int i = 0; i < TethysGame::NoPlayers(); i++)
disasterHelper.AddSafeRect(LOCATIONS_TABLE[i]);
}
Def kinda scratching my head over why I thought using a map was the best option considering that an int was being used as a key...
Anyway, whatever. The vector is a better option so eh.
Was scratching my head over that too. ;)
I would recommend arrays over vectors for two reasons:
- Easier to read/use
- Done right, the memory should be static, so the whole issue of dynamic memory management, and associated performance hits are avoided completely.
I would also do away with the initialization function, and instead initialize the variable where it is declared with an initializer list. This applies to both arrays and vectors, though you need a fairly up to date compiler to do this with vectors. This should be do-able outside of a function, so this will work for both local and global variables.
const int a[] = {1, 2, 3}; // const optional, but very appropriate if the array won't be modified
I haven't compiled this, but here's my rough shot at what the code would look like:
const MAP_RECT safeList[] = {
{ 10 + X_, 10 + Y_, 30 + X_, 30 + Y_ },
{ 100 + X_, 10 + Y_, 30 + X_, 30 + Y_ },
{ 10 + X_, 100 + Y_, 30 + X_, 30 + Y_ },
{ 100 + X_, 100 + Y_, 30 + X_, 30 + Y_ },
};
void setDisasterSafeZones()
{
for (int i = 0; i < TethysGame::NoPlayers(); i++)
disasterHelper.AddSafeRect(safeList[i]);
}
Also, if you are randomizing base locations, which I assume you are, the associated safe locations will need to be randomized as well in the same order. Rather than randomize the base data directly, you can randomize an array (let's say playerSlot) containing integers 0..NumStartingLocations, and use that as an index into both the baseData array, and the safeList array.
int playersSlot[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12 }; // Lots of base locations =)
Export int InitProc() {
RandomizeList(AutoSize(playerSlot));
// ...
// Create bases loop
CreateBase(i, startLocation[playerSlot[i]]);
// ...
// Loop to create safe rects
disasterHelper.AddSafeRect(safeList[playerSlot[i]]);
Okay,
Thank you for everyones' replies here and in the other Disaster thread. I think in order to stay consistent with how BaseBuilder is working, I'll use standard c arrays for the example code. Of course whoever uses the class can initialize it using std::array or std::vector if they want instead.
I added a constant for the default time in marks. I understand that ticks are used in a lot of the program. TethysGame disaster function durations are set in marks, so using marks as a default unit in DisasterHelper seemed natural. It is also natural for me when I want to think about durations of disasters to think in the broad strokes of marks instead of micro stroke of ticks. But the conversion is easy, so probably not worth dwelling on one way or another.
static const int TimerNeverExpires = INT_MAX;
static const int TimerDefaultValue = 250; //In Marks
I'd like to stick with CamelCase for the static variables. I think Leeor would prefer to see CAMEL_CASE. My disagreement is probably my C# background coming through. I did a search for static const variables in the SDK and didn't really find any. If there is a strong preference towards ONLY_CAPITAL, then I will switch to it so as to keep coding style similar across the SDK.
I modified the code in the first post to include the new Timer content. I also re-wrote the example code. There are now 3 use examples.
- Pairing the Disaster Helper with BaseHelper in a multiplayer type scenario. Safe Areas are manually set.
- Using a function to auto find Command Centers and adding MAP_RECTs of a pre-determined size to DisasterHelper. Sort of a hands off approach.
- Using DisasterHelper in a colony game scenario where dealing with Saving/Loading games must be taken into account.
If there are more suggestions, then please send them along. I think it is getting time to get the code pushed into a scenario that gets finished and see how it goes. I'm kind of lamenting not being familiar with BaseBuilder now since I want to see how DisasterHelper fits in with it. I'm working on a single player unit only mission that can sort of test it. The last one standing scenarios that Dave is working on would probably be better though.
Just had a thought on increasing convenience. Instead of having user's write their own code to add safe rects:
void setDisasterSafeZones()
{
for (int i = 0; i < TethysGame::NoPlayers(); i++)
disasterHelper.AddSafeRect(safeList[i]);
}
You could provide a method on the disaster helper class to add an array of safe rects.
disasterHelper.AddSafeRects(TethysGame::NoPlayers(), safeList);
It's probably easier for the user to define an array of safe zones, so just let them set that directly, rather than force them to write a loop. Note that I named the function AddSafeRects above. An alternative might be SetSafeRects if you want to clear the list and then add (or just store a pointer to the passed list to avoid copying, though that's less encapsulated and would need to be documented).
Hooman,
Thanks for the suggestion. How about the code below? It will let you add SafeRects individually, as c style arrays, as std::arrays, and as std::vectors.
I think in this case it makes since to leave the safeRects strongly encapsulated and not use pointers. The user shouldn't need to pass more than a dozen MAP_RECTs when intializing the scenario, and this is usually only going to happen once. I like allowing the user to add MAP_RECTs with multiple calls instead of resetting them every time the function is called. Although, since the user will often only set the SafeRects by calling the function one time, I can see why more of a set approach would fit nicely.
I just noticed I made a typo and made the class level object safeRects read as SafeRects, so I'll fix that as well and make it safeRects.
Arklon & Hooman,
Thanks for the insights on the different case schemes. What you two suggested is probably where I'll try to stay.
void ClearSafeRects()
{
safeRects.clear();
}
//Add a MAP_RECT where no earthquakes or large meteors will be created.
//This is designed to protect starting locations from powerful disasters.
void AddSafeRect(const MAP_RECT& safeRect)
{
safeRects.push_back(safeRect);
}
//Add a c style array of MAP_RECTs where no earthquakes or large meteors will be created.
//This is designed to protect starting locations from powerful disasters.
void AddSafeRects(int arraySize, MAP_RECT safeRects[])
{
for (int i = 0; i < arraySize; i++)
{
this->safeRects.push_back(safeRects[i]);
}
}
template<typename MAP_RECT, size_t N>
void AddSafeRects(const std::array<MAP_RECT, N>& safeRects)
{
this->safeRects.insert(this->safeRects.end(), safeRects.begin(), safeRects.end());
}
void AddSafeRects(const std::vector<MAP_RECT>& safeRects)
{
this->safeRects.insert(this->safeRects.end(), safeRects.begin(), safeRects.end());
}
* Edit: fixed an error I found in the template version of the function.
Also thought I'd add a note about the AutoSize macro from OP2Helper.
You can add as many safe rects as players using this code:
disasterHelper.AddSafeRects(TethysGame::NoPlayers(), safeList);
You can add all safe rects using this code:
disasterHelper.AddSafeRects(AutoSize(safeList));
AutoSize macro expands to a calculated array size (works for static arrays), followed by an array pointer. There was a reason I put the arguments in that order. ;)