Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ebfb7d0
Implement a spawn.ini reader
ZivDero Aug 28, 2026
4feddad
Merge branch 'main' into cncnet
ZivDero Aug 29, 2026
5eec22b
Client launch (#75)
ZivDero Aug 29, 2026
d9e326d
Merge branch 'main' into cncnet
ZivDero Aug 29, 2026
bc471c8
Pin what the client launch path promises
ZivDero Aug 29, 2026
e3226f0
Hold the campaign difficulty to the settings the game has
ZivDero Aug 29, 2026
28cb381
Carry the campaign handicap pair on the session
ZivDero Aug 29, 2026
dc8c57e
Launch a campaign mission from a client's file
ZivDero Aug 29, 2026
8961ed2
Merge branch 'main' into cncnet
ZivDero Aug 30, 2026
91ac18b
Merge branch 'main' into cncnet
ZivDero Aug 30, 2026
d226c5d
Resume a saved game from a spawn request
ZivDero Aug 30, 2026
7960033
Do not repeat a score the game does not have
ZivDero Aug 30, 2026
be31071
Merge branch 'main' into cncnet
ZivDero Aug 30, 2026
4bcb208
Assemble a game against other machines up to its sockets
ZivDero Aug 30, 2026
fdcdb39
Wire a spawned game against other machines to its network
ZivDero Aug 30, 2026
45605e0
Resume a saved game against other machines from a launch file
ZivDero Aug 30, 2026
b246350
Trim the client launch comments to what the code cannot show
ZivDero Aug 30, 2026
f04bac9
Condense the client launch change records
ZivDero Aug 30, 2026
74bd1d0
Credit the spawners the launch file vocabulary comes from
ZivDero Aug 30, 2026
b665f76
Shorten the client launch change records
ZivDero Aug 30, 2026
75a5d9d
Merge branch 'main' into cncnet
ZivDero Aug 30, 2026
e877e07
Stop reading the launch keys the game will never take
ZivDero Aug 30, 2026
63bef66
Simplify switch
ZivDero Aug 30, 2026
eca0437
Merge remote-tracking branch 'origin/main' into cncnet
ZivDero Aug 31, 2026
d08d35f
CI fix
ZivDero Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions code/gamedirs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ static char const * const DefaultSearchFolders = "INI,MIX,Maps";

static char const * const ConfigName = "OPENTS.INI";

/*
* The folder saved games are kept in, under whichever directory the player's own files go.
*/
static char const * const SavedGamesFolder = "Saved Games";

/*
* The folders the configuration itself is looked for in, relative to the data directory.
*/
Expand Down Expand Up @@ -314,6 +319,22 @@ std::string User_File_Write_Name(char const * filename)
}


/// <summary>
/// Names a saved game inside the folder they are kept in. That folder is deliberately not
/// one of the searched ones, and is made on the way so a launcher can browse it before the
/// first save is written.
/// </summary>
/// <returns>The name to open, delete or scan for.</returns>
std::string Saved_Game_Name(char const * filename)
{
std::string const folder = UserDirectory + SavedGamesFolder;

CreateDirectory(folder.c_str(), NULL);

return(folder + '\\' + filename);
}


static void Scan_Folder(char const * prefix, char const * pattern, std::vector<std::string> & names)
{
std::string const search = std::string(prefix) + pattern;
Expand Down
6 changes: 6 additions & 0 deletions code/gamedirs.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,11 @@ char const * Game_Directory_Error(void);
*/
std::string User_File_Write_Name(char const * filename);

/*
* Where the player's saved games are. They are never searched for: the folder is named
* outright wherever a saved game is opened, listed or removed.
*/
std::string Saved_Game_Name(char const * filename);

std::vector<std::string> Parse_Search_Folders(char const * list);
std::vector<std::string> Search_Files(char const * pattern);
2 changes: 2 additions & 0 deletions code/house.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ HouseClass::HouseClass(HouseTypeClass const * type) :
BuildingsLost(0),
WhoLastHurtMe(HOUSE_NONE),
Center(0,0,0),
SpawnWaypoint(-1),
Radius(0),
LATime(0),
LAEnemy(HOUSE_NONE),
Expand Down Expand Up @@ -6491,6 +6492,7 @@ void HouseClass::Serialize(SaveStreamClass & stream)
stream.Serialize(EnemyAirForcePrediction);
stream.Serialize(EnemyInfantryForcePrediction);
stream.Serialize(PowerSurplus);
stream.Serialize(SpawnWaypoint);
}


Expand Down
1 change: 1 addition & 0 deletions code/house.h
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ class HouseClass : public AbstractClass
** the base.
*/
Coord Center; // Center of the base.
int SpawnWaypoint; // starting waypoint this house was placed at; -1 = never placed
int Radius; // Average building distance from center (leptons).
struct {
int AirDefense;
Expand Down
108 changes: 67 additions & 41 deletions code/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
#include "scheme.h"
#include "script.h"
#include "session.h"
#include "spawner.h"
#include "side.h"
#include "skirmish.h"
#include "smudtype.h"
Expand Down Expand Up @@ -412,18 +413,20 @@ int Init_Game(int , char * [])
/*
** Play the startup animation.
*/
if (Special.IsFromInstall == true) {
DebugString("Playing first time intro sequence.\n");
Play_Movie("EVA.VQA", THEME_NONE, false);
}
if (!Spawner_Is_Requested()) {
if (Special.IsFromInstall == true) {
DebugString("Playing first time intro sequence.\n");
Play_Movie("EVA.VQA", THEME_NONE, false);
}

DebugString("Playing startup movies.\n");
Play_Movie("WWLOGO.VQA", THEME_NONE);
if (!Get_New_Menu()->MixFile) {
if (CCFileClass("FS_TITLE.VQA").Is_Available() == true) {
Play_Movie("FS_TITLE.VQA", THEME_NONE, false);
} else {
Play_Movie("STARTUP.VQA", THEME_NONE, false);
DebugString("Playing startup movies.\n");
Play_Movie("WWLOGO.VQA", THEME_NONE);
if (!Get_New_Menu()->MixFile) {
if (CCFileClass("FS_TITLE.VQA").Is_Available() == true) {
Play_Movie("FS_TITLE.VQA", THEME_NONE, false);
} else {
Play_Movie("STARTUP.VQA", THEME_NONE, false);
}
}
}

Expand Down Expand Up @@ -647,6 +650,21 @@ void Init_Campaigns(void)
}


/// <summary>
/// Reads the countries and the sides they belong to from the rules, so a house's side is
/// known before anything asks for it. Only this reading establishes the roster.
/// </summary>
void Prepare_Side_Roster(void)
{
Rule->Do_HouseTypes(*RuleINI);
Rule->Do_Sides(*RuleINI);

for (int index = 0; index < HouseTypes.Count(); index++) {
HouseTypes[index]->Read_INI(*RuleINI);
}
}


/// <summary>
/// Can this campaign be played with the addons that are enabled?
/// A base game campaign is offered only when no addon is running, and an addon's own
Expand Down Expand Up @@ -1071,6 +1089,18 @@ bool Select_Game(bool )
}
}

/*
* A launch takes the place of the menu once; an ended match or a refusal answers false,
* so the process leaves and the client sees it go.
*/
if (Spawner_Is_Requested()) {
if (!Spawner_Prepare(gameloaded)) {
return(false);
}
process = false;
Theme.Stop(true);
}

while (process) {

/*
Expand Down Expand Up @@ -1117,33 +1147,6 @@ bool Select_Game(bool )
break;
}

switch (Options.Difficulty) {
case 0:
Scen->CDifficulty = DIFF_HARD;
Scen->Difficulty = DIFF_EASY;
break;

case 1:
Scen->CDifficulty = DIFF_HARD;
Scen->Difficulty = DIFF_NORMAL;
break;

case 2:
Scen->CDifficulty = DIFF_NORMAL;
Scen->Difficulty = DIFF_NORMAL;
break;

case 3:
Scen->CDifficulty = DIFF_EASY;
Scen->Difficulty = DIFF_NORMAL;
break;

case 4:
Scen->CDifficulty = DIFF_EASY;
Scen->Difficulty = DIFF_HARD;
break;
}

Theme.Stop(true);

int timeout = (TickCount + 5 * TIMER_SECOND);
Expand Down Expand Up @@ -1386,8 +1389,14 @@ bool Select_Game(bool )
Session.PlayerIsGDI = stricmp(HouseTypes[Session.Players[0]->Player.House]->Name(), "GDI") == 0;
}

if (Session.Type != GAME_NORMAL || Debug_ForceScenario || Session.Play) {
if (!Start_Scenario(Scen->ScenarioName, true, CAMPAIGN_NONE)) {
// The menu sets the pair on every path but a client launch, which chose it itself.
if (!Spawner_Is_Active()) {
Session.CampaignDifficulty = (DiffType)Options.Difficulty;
Session.CampaignCDifficulty = (DiffType)(DIFF_COUNT - 1 - Options.Difficulty);
}

if (Session.Type != GAME_NORMAL || Debug_ForceScenario || Session.Play || Spawner_Is_Active()) {
if (!Start_Scenario(Scen->ScenarioName, true, Spawner_Is_Active() ? Scen->Campaign : CAMPAIGN_NONE)) {
if (Debug_Map) {
return(false);
} else {
Expand All @@ -1404,6 +1413,13 @@ bool Select_Game(bool )
}
}

// The mission read clears these, so a launch file's carried-over flags are set after it.
if (Spawner_Is_Active() && Session.Type == GAME_NORMAL) {
for (int index = 0; index < ARRAY_SIZE(Environment.Globals); index++) {
Scen->Set_Global_To(index, Environment.Globals[index]);
}
}

/*
** Save initialization values if we're recording this game.
*/
Expand Down Expand Up @@ -1655,6 +1671,12 @@ bool Parse_Command_Line(int argc, char * argv[])
continue;
}

// A client asking the game to launch what SPAWN.INI describes.
if (stricmp(string, "-SPAWN") == 0) {
Spawner_Request();
continue;
}

if (memcmp(string, "-TIME=", 6) == 0) {
sscanf(&string[6], "%d", &TournamentTime);
}
Expand Down Expand Up @@ -1830,7 +1852,11 @@ void Init_Random(void)
** a recording; the random number generator is initialized by loading
** the game.
*/
if (Session.LoadGame || Session.Play) {
if (Session.LoadGame) {
return;
}

if (Session.Play) {
Scen->RandomNumber = Seed;
NonCriticalRandomNumber = Seed;
DebugString("Seed is %08x\n", Seed);
Expand Down
2 changes: 2 additions & 0 deletions code/init.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ void Title_Screen_Restore(bool force=false);

void Init_Campaigns(void);

void Prepare_Side_Roster(void);

void Delete_All_Objects(void);

void Init_Theater(TheaterType theater);
Expand Down
16 changes: 10 additions & 6 deletions code/language/language.rc
Original file line number Diff line number Diff line change
Expand Up @@ -866,16 +866,18 @@ BEGIN
47,97,115,14
END

IDD_OPT_CTRL_MP DIALOG DISCARDABLE 0, 0, 209, 75
IDD_OPT_CTRL_MP DIALOG DISCARDABLE 0, 0, 209, 93
STYLE WS_CHILD
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "Resume Mission",IDC_RESUME_MISSION,"Button",
BS_OWNERDRAW,55,48,99,14
BS_OWNERDRAW,55,66,99,14
CONTROL "Game Controls",IDC_GAME_CONTROLS,"Button",BS_OWNERDRAW,
55,12,99,14
CONTROL "Abort Mission",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW,
CONTROL "Save Game",IDC_SAVE_GAME,"Button",BS_OWNERDRAW,
55,30,99,14
CONTROL "Abort Mission",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW,
55,48,99,14
END

IDD_SERIAL_PHONE_LIST DIALOG DISCARDABLE 0, 0, 344, 167
Expand Down Expand Up @@ -1426,11 +1428,13 @@ STYLE WS_CHILD
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "Resume Mission",IDC_RESUME_MISSION,"Button",
BS_OWNERDRAW,120,48,99,14
BS_OWNERDRAW,120,59,99,14
CONTROL "Game Controls",IDC_GAME_CONTROLS,"Button",BS_OWNERDRAW,
120,12,99,14
120,8,99,14
CONTROL "Save Game",IDC_SAVE_GAME,"Button",BS_OWNERDRAW,
120,25,99,14
CONTROL "Abort Mission",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW,
120,30,99,14
120,42,99,14
CONTROL "Slider1",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32",
TBS_BOTH | TBS_NOTICKS,95,115,148,13
LTEXT "Game Speed",-1,39,115,58,13,SS_CENTERIMAGE | NOT
Expand Down
Loading
Loading