There has allegedly been an effort toward making it accessible via python scripting (glares at Arklon and BlackBox) but that seems to be an echo on the wind.That's still in development, but it's also larger in scope than just for writing missions. As far as missions goes, it's basically done, except for the parts of the public API we never bothered to document like Pinwheel groups, and exposing more non-public API game internals (though quite a bit is already exposed).
I've manged to create simple DLLs with other compilers before, that the game sees as a level, and can load them, but they've never been able to do anything useful, like create initial units. The name mangling used by the C++ functions that Outpost2.exe exports always got in the way. The data exports (from the DLL) were simple enough to get the game to recognize the DLLs, but the function exports (from Outpost2.exe) limited the DLL from doing anything useful.He worked around this problem by just making extern C wrappers of all the OP2 API functions. I remember someone telling me that C++.NET/CLR is fully ABI compatible with C++ though (they're both still built using the MS toolchain so that wouldn't be a shock), so there's probably a better way to do this. That said, the general idea of using C++.NET as an interop shim between C++ and C# is actually a pretty good practice, and also the only real good use case for C++.NET.
We've been pushing to migrate everything to GitHub to be as public as possible with it. Would you consider doing the same? Or would you prefer that we integrate this into the OutpostUniverse organization on GitHub?
What about saving and then loading a game? Outpost 2 requires all data to be accessible in a fixed sized buffer, and there's no notification of when it's going to save or load data to/from that buffer, so it needs to be the live data.
Yeah, I'm liking this idea of using C# for mission development. I guess it makes sense there would be some kind of interop.
I was looking through the code a bit the other night. Looks like the C# classes are proxies for the C++ classes. The C++ classes are in turn proxies for internal Outpost2.exe classes. I'm wondering if we can do something more direct there. Perhaps there's a way that adds less boilerplate, or that might make the Save/Load thing easier. The game really could have made use of Save/Load methods, or at least callback notifications. It would have made the system much more flexible. I'd love to see this up on GitHub so we could play around with this C# idea some more. :)
And Triggers, oh boy.
"PlayerID":0,
"BaseCenterPt": { "X":27, "Y":38 },
"Units":
[
{ "TypeID":"CommandCenter", "MinDistance":1 },
{ "TypeID":"StructureFactory", "MinDistance":0 },
{ "TypeID":"ConVec", "CargoType":"CommandCenter", "MinDistance":1, "SpawnDistance":3 },
{ "TypeID":"ConVec", "CargoType":"None","MinDistance":1, "SpawnDistance":3 },
{ "TypeID":"CommonOreSmelter", "MinDistance":0 },
{ "TypeID":"Agridome", "MinDistance":0 },
{ "TypeID":"Agridome", "MinDistance":1 },
{ "TypeID":"GORF", "MinDistance":0 },
{ "TypeID":"Residence", "MinDistance":1 },
{ "TypeID":"StandardLab", "MinDistance":0 },
{ "TypeID":"Nursery", "MinDistance":0 },
{ "TypeID":"Residence", "MinDistance":1 },
{ "TypeID":"University", "MinDistance":0 },
{ "TypeID":"RobotCommand", "MinDistance":0 },
{ "TypeID":"Residence", "MinDistance":1 },
{ "TypeID":"MedicalCenter", "MinDistance":1 },
{ "TypeID":"DIRT", "MinDistance":0 },
{ "TypeID":"Agridome", "MinDistance":1 },
{ "TypeID":"MedicalCenter", "MinDistance":1 },
{ "TypeID":"RecreationFacility", "MinDistance":0 },
{ "TypeID":"RecreationFacility", "MinDistance":1 },
{ "TypeID":"DIRT", "MinDistance":2 },
{ "TypeID":"VehicleFactory", "MinDistance":1, "CreateWall":true },
{ "TypeID":"Tokamak", "MinDistance":3 },
{ "TypeID":"Tokamak", "MinDistance":2 },
{ "TypeID":"GuardPost", "CargoType":"Microwave","MinDistance":2, "CreateWall":true },
{ "TypeID":"GuardPost", "CargoType":"EMP", "MinDistance":2, "CreateWall":true },
{ "TypeID":"GuardPost", "CargoType":"Microwave","MinDistance":2, "CreateWall":true },
{ "TypeID":"GuardPost", "CargoType":"ESG", "MinDistance":2, "CreateWall":true },
{ "TypeID":"GuardPost", "CargoType":"RPG", "MinDistance":2, "CreateWall":true },
{ "TypeID":"GuardPost", "CargoType":"EMP", "MinDistance":2, "CreateWall":true },
{ "TypeID":"GuardPost", "CargoType":"RPG", "MinDistance":2, "CreateWall":true },
{ "TypeID":"Tokamak", "MinDistance":3 }
]
/bin/sh: 2: /tmp/tmpcac72cbbf94540508eaf575e65118431.exec.cmd: copy: not found
/.../OP2DotNetMissionSDK/DotNetMissionSDK/DotNetMissionSDK.csproj(8,5): error MSB3073: The command "copy "/.../OP2DotNetMissionSDK/DotNetMissionSDK/bin/Debug/netstandard2.0/DotNetMissionSDK.dll" "*Undefined*Outpost2\DotNetMissionSDK.dll"" exited with code 127.
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)Outpost2\$
(TargetFilename)"" />
</Target>
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(SolutionDir)Outpost2" />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(ProjectDir)../Outpost2" />
dotnet build DotNetMissionSDK
// Calculate map size and determine if map wraps
// Clip to top left
LOCATION mapSize = new LOCATION();
mapSize.ClipToMap();
// Initialize to top left corner
area = new MAP_RECT(mapSize, mapSize);
for (int i=0; i < MaxMapSize+10; ++i)
{
int x = mapSize.x;
int y = mapSize.y;
++mapSize.x;
++mapSize.y;
mapSize.ClipToMap();
// Clamped map will pull mapSize.x back to x.
// The max value is the current and previous value.
if (mapSize.x == x)
{
area.maxX = x;
doesWrap = false;
}
// Unclamped map will wrap mapSize.x back to 0.
// The max value is the previous value.
if (mapSize.x == 0)
{
area.maxX = x;
doesWrap = true;
}
// Y is always clamped.
// When mapSize.y is pulled back to y, and we have our clamped or wrapped values, end search.
if ((mapSize.x == x || doesWrap) && mapSize.y == y)
{
area.maxY = y;
break;
}
}
for (int i = 0; i < container.size(); ++i)
The biggest thing I'm thinking about for how I would actually use this function is that, in most scenarios, there will be one or two structures you want to demand are put in a particular place. Smelters come to mind here, as well as enemy structures that are mission objectives in some way. If you use the manual structure placement you mentioned before, will it know to build a tube from the final autolayout-ed base to wherever the manually placed structure is, or does it simply know not to cause unit overlaps with it?You make a good point.
It's pretty easy and standard to write 0-based array code that is start point inclusive, and end point exclusive.Saying this made me rethink using max inclusive. I looked at some other frameworks, and apparently you are right, it is standard to use max exclusive. It seems strange to me that a rect contains a point on its min edge, but not its max edge. However, I also think that a rect of zero size shouldn't be able to contain a point, even if it is infinitely small. Anyway, it's been standardized.
[snip]
PopulationGrowth, // Bot focuses on growing population. Keeps enough defense to avoid being killed. Will build Recreation, DIRT and other optional structures.
LaunchStarship, // Bot focuses on launching starship. Keeps enough defense to avoid being killed.
EconomicGrowth, // Bot focuses on resource acquisition. Keeps enough defense to avoid being killed.
Passive, // Bot does not build new structures. Keeps enough defense to avoid being killed.
Defender, // Bot will build military units and defend itself and allies. Does not attack.
Balanced, // Bot will build military units and defend itself and allies. Attacks with best available strategy.
Aggressive, // Bot will build military units and won't defend itself or allies. Attacks with best available strategy.
Harassment, // Bot will build military units and harass trucks, power plants, and unescorted or poorly defended utility vehicles.
Wreckless, // Bot will build military units and send them to attack even against overwhelming odds.
CreateConvecTask : Task
{
VehicleFactoryTask m_Prerequisite;
Task m_Parent;
if (!m_Prerequisite.IsTaskComplete())
{
if (IsAncestorTask(m_Prerequisite))
return false; // Fail out
}
bool IsAncestorTask(prerequisite)
{
if (m_Parent == null)
return false;
if (prerequisite.GetType() == m_Parent.GetType())
return true;
return m_Parent.IsAncestorTask(prerequisite);
}
}
public abstract class Task
{
private Task m_Parent;
protected Player m_Owner;
protected Task[] m_Prerequisites = new Task[0];
public Task(Player owner, Task parent)
{
m_Owner = owner;
m_Parent = parent;
}
public abstract bool IsTaskComplete();
public virtual bool PerformTask()
{
if (!PerformPrerequisites())
return false;
return true;
}
private bool PerformPrerequisites()
{
for (int i=0; i < m_Prerequisites.Length; ++i)
{
/* Skip completed tasks */
if (m_Prerequisites[i].IsTaskComplete())
continue;
/* If a prerequisite is an ancestor task, then we have a circular dependency. This task cannot be performed. */
if (IsAncestorTask(m_Prerequisites[i]))
return false;
/* Perform the task. Fail out if it can't be done. */
if (!m_Prerequisites[i].PerformTask())
return false;
}
return true;
}
private bool IsAncestorTask(Task task)
{
if (m_Parent == null)
return false;
if (task.GetType() == m_Parent.GetType())
return true;
return m_Parent.IsAncestorTask(task);
}
}
TechCor, there is a new PR in OP2MissionSDK that pulls in new library changes. Wanted to bring it to your attention before merging since I think you are actively using the library. Unless you are using deprecated portions, I do not anticipate any issues (famous last words). Also opened a new forum thread if you want to post any concerns, additions, etc.Sounds good. I'm watching the other thread and will get the changes when they are ready.
Thanks,
Brett
void SetFactoryCargo(int bay, map_id unitType, map_id cargoOrWeaponType); // [StructureFactory, Spaceport] [Note: If items is an SULV, RLV, or EMP Missile, it is placed on the launch pad instead of in the bay]
// Specific Building
map_id GetObjectOnPad() const; // [Spaceport]
void DoLaunch(int destPixelX, int destPixelY, int bForceEnable); // [Spaceport]
void PutInGarage(int bayIndex, int tileX, int tileY); // [Garage]
int HasOccupiedBay() const; // [Garage, StructureFactory, Spaceport]
void SetFactoryCargo(int bay, map_id unitType, map_id cargoOrWeaponType); // [StructureFactory, Spaceport] [Note: If items is an SULV, RLV, or EMP Missile, it is placed on the launch pad instead of in the bay]
void DoDevelop(map_id itemToProduce); // [Factory] [Note: Sets weapon/cargo to mapNone, can't build Lynx/Panther/Tiger/GuardPostKits]
void ClearSpecialTarget(); // [Lab]
int PlaybackCommand(struct CommandPacket *, int);
You can always submit a pull request for HFL updates.Level DLLs are actually the most susceptible to the not being relocatable thing, since they are loaded last.
Additionally, OP2Internal can be used with level building. Though it does currently complicate linking, and make the module not relocatable. The relocatable thing isn't normally a problem in practice though, particularly not for level DLLs. I'm planning to address those issues soon.
Launch pad rocket is an int32 at +0x6E from the base of the internal unit structure, launch cargo (starship module loaded in rocket) is an int32 at +0x72.How did you know it was an int32? I was having problems building a SULV after launching hacked cargo. Turns out changing the value from short to int fixed the problem.
Unit:Building:Spaceport
-----------------------
0x6E 4 launchPadUnitType [SULV/RLV/EMPMissile]
0x72 2 launchCargo
0x74 2 boolRLVLanding
Type: (0x06) "ctMoBuild" Build a building (ConVec, RoboMiner, GeoCon)
------------
Size in bytes: 0x13 (19 bytes) minimum to deploy RoboMiner/GeoCon/ConVec building
Size in bytes: 0xD + numUnits*2 + numWayPoints*4
(13 + numUnits*2 + numWayPoints*4)
Offset Size Description
------ ---- -----------
0x0 1 numUnits
0x1 2*X unitID - array of size numUnits
0x3 2 numWayPoints (BYTE)
0x5 4*Y wayPointLocation - (pixelX:15, pixelY:14)
0x9 2 buildArea.TopLeft.x (bulldozed tile)
0xB 2 buildArea.TopLeft.y (bulldozed tile)
0xD 2 buildArea.BottomRight.x (bulldozed tile)
0xF 2 buildArea.BottomRight.y (bulldozed tile)
0x11 2 (Stored to Unit.+0x6A - value 0xFFFF)
(pixelX & 0x07FFF) | (pixelY & 0x03FFF) << 15;
struct Waypoint {
int x:15;
int y:14;
int reserved:3;
};
Waypoint waypoint{pixelX, pixelY};
In the UnitInfo class in HFL, there is a function called GetResearchTopic(). The problem is I don't know what this is returning or if it is valid. It returns 69 for the Spaceport, but "Space Program" has tech ID of 5405.That's because the tech index in that context is by array index, which boils down to the order in which techs are defined in the tech .txt, rather than tech ID.
I've tried looking at OllyDbg, but it doesn't appear to match what HFL is doing at all. I'm thinking it's a discrepancy between unit types (vehicles and structures). HFL seems to assume all units are the same size struct.The unit entries in the internal unit array are all the same size, based on sizeof mapGeneralUnit (max map_id) I believe.
GameMap::SetCellType(loc, CellTypes::cellRubble);
GameMap::SetTile(loc, changeSet.ReplacementTileID(TethysGame::GetRand(changeSet.ReplacementTileRange())));
public void Main.Update()
{
/* First thing called on the first update frame after mission init.
Will crash inside Outpost2.exe::HasTechnology()*/
TethysGame.GetPlayer(TethysGame.LocalPlayer()).HasTechnology(1);
}
UniversityState university = stateSnapshot.players[0].units.universities[0];
if (stateSnapshot.players[0].commandMap.ConnectsTo(university.GetRect()))
// university connects to a command center, do something
UniversityState university = stateSnapshot.players[0].units.universities[0];
if (stateSnapshot.players[0].commandMap.ConnectsTo(university.GetRect()))
GameState.GetUnit(university.unitID).DoTrainScientists(Math.Min(stateSnapshot.players[0].workers, 10));
ThreadAssert.MainThreadRequired();
if (m_IsProcessing)
return;
m_IsProcessing = true;
AsyncPump.Run(() =>
{
List<Action> actions = new List<Action>();
// Do async tasks
UniversityState university = stateSnapshot.players[0].units.universities[0];
if (stateSnapshot.players[0].commandMap.ConnectsTo(university.GetRect()))
{
// Add command to list for processing on main thread.
// The ? in the command checks if unit is null before executing DoTrainScientists.
// GameState.GetUnit will return null if the unit has been destroyed.
// This may happen due to the Time() difference between the snapshot time and when the command is finally executed.
actions.Add(() => GameState.GetUnit(university.unitID)?.DoTrainScientists(Math.Min(stateSnapshot.players[0].workers, 10)));
}
return actions;
},
(object returnState) =>
{
m_IsProcessing = false;
// Execute all completed actions
List<Action> actions = (List<Action>)returnState;
foreach (Action action in actions)
action();
});
No no, we're talking about defending with starflares, not attacking with them. ;)
A bit off topic here, but someone should totally make a map based on that idea. Disable all weapons except for starflares, so the player is forced to defend with them. Then an AI can send wave after wave of their own tanks at the player's base. ;)The AI will take control of anything it is given. Combat Management is separate from Base Management, so you don't need any structures for it to work. However, if they happen to have a base they might prioritize defending (which could be a good thing!).
Speaking of AI, could the AI code here handle attack waves coming in from off the side of the map? :)
Sounds like a fairly passive AI. Are there plans on having different 'personalities'? Age of Empires had several AI personalities that would range from extremely passive to extremely aggressive, it made for more interesting single-player and even multi-player campaigns.Please understand this is a lot of work and not even close to done.
1. How does it decide to focus on building a specific combat unit. Ie should I build a laser lynx, a railgun lynx, or a starflare lynx? Each are similarly priced but each serves a different combat role. Or maybe decide to save up for a Laser Tiger instead?
2. What happens when no one focuses on military? Does no AI become proactive and start building a military or do they all stay in colony mode?
3. Do the AIs suffer from morale and thus from blowing up an enemy's Nursery and the massive morale penalty that follows? Or Morale in general, and thus the need to keep a food surplus, sufficient residences, or an active nursery/university?
5. Can the AI's people die? Either from starvation, or natural causes or unnatural causes such as a starflare blowing up their workplace?
4. Do the AIs spend any time on preparing for disasters? ie if an AI had resources for lava walls, would they build them to protect their base from getting melted?
1. Does the AI use group tactics, or just sends individual units to a spot? From watching the video, it seems each individual unit is sent to a spot.Combat is not finished yet, but it will have group and individual tactics. Currently, groups stage outside a Threat Zone before entering, but it does not work properly at all. Group tactics are essentially "send this group of units to this zone through this path", individual tactics will be "ESG does not shoot structures", "starflares attack between groups of units", and "stay just out of range of enemy fire, if possible".
2. Where on the list of priorities, does repairing damage fall in? ie a meteor hits a residence; does the AI prioritize resources to building lynxes to fight the enemy pounding on their turrets or do they spend their resources repairing the residence?AI prioritizes repairs to structures that have been critically damaged / disabled if it is required to complete another task. For example, if the task is to build a laser lynx, but the vehicle factory is crippled, it will prioritize repairing the factory. Same deal with tubing. If the residence was destroyed, cutting off tube access to the factory, it will prioritize reconnecting the factory. What gets repaired/reconnected/rebuilt first, if more than one building is crippled/disconnected/destroyed, is based on relative goal priority/importance.
3. Similarly, will units use a Garage when damaged?Garages have not been added to the task tree yet. They are not like other structures and need very special handling. It will likely end up as a separate goal.
4. Or a better question, will units retreat after taking X damage, so that they might survive to return to repair?Combat is very rudimentary right now. I'd like to get them attacking properly before worrying about retreat/repair behavior. I think I have assault groups set up to take a repair unit for every 10th unit so they can repair during the assault, but individual tactics haven't been worked on yet.
5. How does the Ai handle earning enemy units via spider hacking? As the unit wasn't built, and its allegiance used to be another player...No individual tactics yet. Spiders can't reprogram. Captured units, or combat units created by the mission developer, are processed by the combat manager like any other combat unit. Supported unit types for slots include both colonies. Cargo trucks, convecs, etc, are used by the base manager if it can find a use for them.
A bit off topic here, but someone should totally make a map based on that idea. Disable all weapons except for starflares, so the player is forced to defend with them. Then an AI can send wave after wave of their own tanks at the player's base. ;)Commander: