Disable passwords, add list of missions completed to autosave

This commit is contained in:
Cong
2021-06-06 12:37:33 +10:00
parent 0025a7e01b
commit 4250765b8c
17 changed files with 342 additions and 574 deletions

View File

@@ -2,7 +2,7 @@
C-Dogs SDL
A port of the legendary (and fun) action/arcade cdogs.
Copyright (c) 2013-2016, 2019 Cong Xu
Copyright (c) 2013-2016, 2019, 2021 Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -43,29 +43,39 @@
#include <emscripten.h>
#endif
#define VERSION 3
Autosave gAutosave;
void MissionSaveInit(MissionSave *ms)
void CampaignSaveInit(CampaignSave *ms)
{
memset(ms, 0, sizeof *ms);
ms->IsValid = 1;
ms->IsValid = true;
CArrayInit(&ms->MissionsCompleted, sizeof(int));
CArrayInit(&ms->Players, sizeof(PlayerSave));
}
bool CampaignSaveIsValid(const CampaignSave *cs)
{
return cs != NULL && cs->IsValid && strlen(cs->Campaign.Path) > 0;
}
void AutosaveInit(Autosave *autosave)
{
memset(&autosave->LastMission.Campaign, 0, sizeof autosave->LastMission.Campaign);
autosave->LastMission.Campaign.Mode = GAME_MODE_NORMAL;
strcpy(autosave->LastMission.Password, "");
CArrayInit(&autosave->Missions, sizeof(MissionSave));
memset(autosave, 0, sizeof *autosave);
autosave->LastCampaignIndex = -1;
CArrayInit(&autosave->Campaigns, sizeof(CampaignSave));
}
void AutosaveTerminate(Autosave *autosave)
{
CA_FOREACH(MissionSave, m, autosave->Missions)
CA_FOREACH(CampaignSave, m, autosave->Campaigns)
CampaignEntryTerminate(&m->Campaign);
CArrayTerminate(&m->MissionsCompleted);
CArrayTerminate(&m->Players);
CA_FOREACH_END()
CArrayTerminate(&autosave->Missions);
CArrayTerminate(&autosave->Campaigns);
}
static void LoadCampaignNode(CampaignEntry *c, json_t *node)
@@ -88,27 +98,40 @@ static void AddCampaignNode(CampaignEntry *c, json_t *root)
json_insert_pair_into_object(root, "Campaign", subConfig);
}
static void LoadMissionNode(MissionSave *m, json_t *node)
static void LoadMissionNode(CampaignSave *m, json_t *node, const int version)
{
MissionSaveInit(m);
CampaignSaveInit(m);
LoadCampaignNode(&m->Campaign, json_find_first_label(node, "Campaign")->child);
strcpy(m->Password, json_find_first_label(node, "Password")->child->text);
LoadInt(&m->MissionsCompleted, node, "MissionsCompleted");
LoadInt(&m->NextMission, node, "NextMission");
if (version < 3)
{
int missionsCompleted = 0;
LoadInt(&missionsCompleted, node, "MissionsCompleted");
for (int i = 0; i < missionsCompleted; i++)
{
CArrayPushBack(&m->MissionsCompleted, &i);
}
m->NextMission = missionsCompleted;
}
else
{
LoadIntArray(&m->MissionsCompleted, node, "MissionsCompleted");
}
// Check that file exists
char buf[CDOGS_PATH_MAX];
GetDataFilePath(buf, m->Campaign.Path);
m->IsValid = access(buf, F_OK | R_OK) != -1;
}
static json_t *CreateMissionNode(MissionSave *m)
static json_t *CreateMissionNode(CampaignSave *m)
{
json_t *subConfig = json_new_object();
AddCampaignNode(&m->Campaign, subConfig);
json_insert_pair_into_object(subConfig, "Password", json_new_string(m->Password));
AddIntPair(subConfig, "MissionsCompleted", m->MissionsCompleted);
AddIntPair(subConfig, "NextMission", m->NextMission);
AddIntArray(subConfig, "MissionsCompleted", &m->MissionsCompleted);
return subConfig;
}
static void LoadMissionNodes(Autosave *a, json_t *root, const char *nodeName)
static void LoadMissionNodes(Autosave *a, json_t *root, const char *nodeName, const int version)
{
json_t *child;
if (json_find_first_label(root, nodeName) == NULL)
@@ -118,21 +141,23 @@ static void LoadMissionNodes(Autosave *a, json_t *root, const char *nodeName)
child = json_find_first_label(root, nodeName)->child->child;
while (child != NULL)
{
MissionSave m;
LoadMissionNode(&m, child);
AutosaveAddMission(a, &m);
CampaignSave m;
LoadMissionNode(&m, child, version);
AutosaveAddCampaign(a, &m);
child = child->next;
}
}
static void AddMissionNodes(Autosave *a, json_t *root, const char *nodeName)
{
json_t *missions = json_new_array();
CA_FOREACH(MissionSave, m, a->Missions)
CA_FOREACH(CampaignSave, m, a->Campaigns)
json_insert_child(missions, CreateMissionNode(m));
CA_FOREACH_END()
json_insert_pair_into_object(root, nodeName, missions);
}
static CampaignSave *FindCampaign(Autosave *autosave, const char *path, int *missionIndex);
void AutosaveLoad(Autosave *autosave, const char *filename)
{
FILE *f = fopen(filename, "r");
@@ -149,15 +174,27 @@ void AutosaveLoad(Autosave *autosave, const char *filename)
printf("Error parsing autosave '%s'\n", filename);
goto bail;
}
// Note: need to load missions before LastMission because the former
// will overwrite the latter, since AutosaveAddMission also
// writes to LastMission
LoadMissionNodes(autosave, root, "Missions");
if (json_find_first_label(root, "LastMission"))
int version = 2;
LoadInt(&version, root, "Version");
LoadMissionNodes(autosave, root, "Missions", version);
if (version < 3)
{
if (json_find_first_label(root, "LastMission"))
{
json_t *lastMission = json_find_first_label(root, "LastMission")->child;
json_t *campaign = json_find_first_label(lastMission, "Campaign")->child;
char *path = NULL;
LoadStr(&path, campaign, "Path");
if (path != NULL)
{
FindCampaign(autosave, path, &autosave->LastCampaignIndex);
CFREE(path);
}
}
}
else
{
LoadMissionNode(
&autosave->LastMission,
json_find_first_label(root, "LastMission")->child);
LoadInt(&autosave->LastCampaignIndex, root, "LastCampaignIndex");
}
bail:
@@ -181,9 +218,8 @@ void AutosaveSave(Autosave *autosave, const char *filename)
}
root = json_new_object();
json_insert_pair_into_object(root, "Version", json_new_number("2"));
json_insert_pair_into_object(
root, "LastMission", CreateMissionNode(&autosave->LastMission));
AddIntPair(root, "Version", VERSION);
AddIntPair(root, "LastCampaignIndex", autosave->LastCampaignIndex);
AddMissionNodes(autosave, root, "Missions");
json_tree_to_string(root, &text);
@@ -191,8 +227,8 @@ void AutosaveSave(Autosave *autosave, const char *filename)
fputs(formatText, f);
// clean up
free(formatText);
free(text);
CFREE(formatText);
CFREE(text);
json_free_value(&root);
fclose(f);
@@ -207,7 +243,7 @@ void AutosaveSave(Autosave *autosave, const char *filename)
#endif
}
MissionSave *AutosaveFindMission(Autosave *autosave, const char *path)
static CampaignSave *FindCampaign(Autosave *autosave, const char *path, int *missionIndex)
{
if (path == NULL || strlen(path) == 0)
{
@@ -218,49 +254,74 @@ MissionSave *AutosaveFindMission(Autosave *autosave, const char *path)
// in this form
char relPath[CDOGS_PATH_MAX] = "";
RelPathFromCWD(relPath, path);
CA_FOREACH(MissionSave, m, autosave->Missions)
CA_FOREACH(CampaignSave, m, autosave->Campaigns)
const char *campaignPath = m->Campaign.Path;
if (campaignPath != NULL && strcmp(campaignPath, relPath) == 0)
{
if (missionIndex != NULL)
{
*missionIndex = _ca_index;
}
return m;
}
CA_FOREACH_END()
return NULL;
}
void AutosaveAddMission(Autosave *autosave, MissionSave *mission)
void AutosaveAddCampaign(Autosave *autosave, CampaignSave *cs)
{
MissionSave *existingMission = AutosaveFindMission(
autosave, mission->Campaign.Path);
if (existingMission != NULL)
CampaignSave *existing = FindCampaign(
autosave, cs->Campaign.Path, &autosave->LastCampaignIndex);
if (existing != NULL)
{
CampaignEntryTerminate(&existingMission->Campaign);
CampaignEntryTerminate(&existing->Campaign);
}
else
{
CArrayPushBack(&autosave->Missions, mission);
existingMission =
CArrayGet(&autosave->Missions, autosave->Missions.size - 1);
memset(existingMission, 0, sizeof *existingMission);
CArrayPushBack(&autosave->Campaigns, cs);
autosave->LastCampaignIndex = autosave->Campaigns.size - 1;
existing =
CArrayGet(&autosave->Campaigns, autosave->LastCampaignIndex);
CampaignSaveInit(existing);
}
const int maxMissionsCompleted =
MAX(existingMission->MissionsCompleted, mission->MissionsCompleted);
memcpy(existingMission, mission, sizeof *existingMission);
existingMission->MissionsCompleted = maxMissionsCompleted;
CampaignEntryCopy(&existingMission->Campaign, &mission->Campaign);
CampaignEntryTerminate(&autosave->LastMission.Campaign);
memcpy(&autosave->LastMission, mission, sizeof autosave->LastMission);
CampaignEntryCopy(&autosave->LastMission.Campaign, &mission->Campaign);
existing->NextMission = cs->NextMission;
// Update missions completed
CA_FOREACH(const int, missionIndex, cs->MissionsCompleted)
bool found = false;
for (int i = 0; i < (int)existing->MissionsCompleted.size; i++)
{
if (*(int *)CArrayGet(&existing->MissionsCompleted, i) == *missionIndex)
{
found = true;
break;
}
}
if (!found)
{
CArrayPushBack(&existing->MissionsCompleted, missionIndex);
}
CA_FOREACH_END()
// Sort and remove duplicates
qsort(
existing->MissionsCompleted.data, existing->MissionsCompleted.size, existing->MissionsCompleted.elemSize,
CompareIntsAsc);
CArrayUnique(&existing->MissionsCompleted, IntsEqual);
CampaignEntryCopy(&existing->Campaign, &cs->Campaign);
}
void AutosaveLoadMission(
Autosave *autosave, MissionSave *mission, const char *path)
const CampaignSave *AutosaveGetCampaign(
Autosave *autosave, const char *path)
{
MissionSave *existingMission = AutosaveFindMission(autosave, path);
MissionSaveInit(mission);
if (existingMission != NULL)
{
memcpy(mission, existingMission, sizeof *mission);
CampaignEntryCopy(&mission->Campaign, &existingMission->Campaign);
}
return FindCampaign(autosave, path, NULL);
}
const CampaignSave *AutosaveGetLastCampaign(const Autosave *a)
{
if (a->LastCampaignIndex < 0)
{
return NULL;
}
return CArrayGet(&a->Campaigns, a->LastCampaignIndex);
}

View File

@@ -2,7 +2,7 @@
C-Dogs SDL
A port of the legendary (and fun) action/arcade cdogs.
Copyright (c) 2013-2015, Cong Xu
Copyright (c) 2013-2015, 2021 Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -31,25 +31,32 @@
#include <stddef.h>
#include <cdogs/campaigns.h>
#include <cdogs/player.h>
#include <cdogs/sys_config.h>
#define PASSWORD_MAX 16
#define AUTOSAVE_FILE "autosave.json"
typedef struct
{
char *Guns[MAX_WEAPONS];
CArray ammo; // of int
} PlayerSave;
typedef struct
{
CampaignEntry Campaign;
char Password[PASSWORD_MAX + 1];
int IsValid;
int MissionsCompleted;
} MissionSave;
bool IsValid;
int NextMission;
CArray MissionsCompleted; // of int
CArray Players; // of PlayerSave
} CampaignSave;
void MissionSaveInit(MissionSave *ms);
void CampaignSaveInit(CampaignSave *ms);
bool CampaignSaveIsValid(const CampaignSave *cs);
typedef struct
{
MissionSave LastMission;
CArray Missions; // of MissionSave
int LastCampaignIndex;
CArray Campaigns; // of CampaignSave
} Autosave;
extern Autosave gAutosave;
@@ -58,6 +65,7 @@ void AutosaveInit(Autosave *autosave);
void AutosaveTerminate(Autosave *autosave);
void AutosaveLoad(Autosave *autosave, const char *filename);
void AutosaveSave(Autosave *autosave, const char *filename);
void AutosaveAddMission(Autosave *autosave, MissionSave *mission);
void AutosaveLoadMission(
Autosave *autosave, MissionSave *mission, const char *path);
void AutosaveAddCampaign(Autosave *autosave, CampaignSave *cs);
const CampaignSave *AutosaveGetCampaign(
Autosave *autosave, const char *path);
const CampaignSave *AutosaveGetLastCampaign(const Autosave *a);

View File

@@ -1,5 +1,5 @@
/*
Copyright (c) 2013-2020 Cong Xu
Copyright (c) 2013-2021 Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -187,17 +187,6 @@ GameLoopData *ScreenMissionBriefing(const struct MissionOptions *m)
mData->TitleOpts.Pad.y = y - 25;
}
// Password
if (m->index > 0)
{
sprintf(
mData->Password, "Password: %s", gAutosave.LastMission.Password);
mData->PasswordOpts = FontOptsNew();
mData->PasswordOpts.HAlign = ALIGN_CENTER;
mData->PasswordOpts.Area = gGraphicsDevice.cachedConfig.Res;
mData->PasswordOpts.Pad.y = y - 15;
}
// Description
if (m->missionData->Description)
{
@@ -448,21 +437,15 @@ static void MissionSummaryOnEnter(GameLoopData *data)
MusicPlay(&gSoundDevice, MUSIC_BRIEFING, NULL, NULL);
if (mData->completed && IsPasswordAllowed(mData->c->Entry.Mode))
if (mData->completed && CanLevelSelect(mData->c->Entry.Mode))
{
// Save password
MissionSave ms;
MissionSaveInit(&ms);
CampaignSave ms;
CampaignSaveInit(&ms);
ms.Campaign = mData->c->Entry;
// Don't make password for next level if there is none
int passwordIndex = mData->m->index + 1;
if (passwordIndex == mData->c->Entry.NumMissions)
{
passwordIndex--;
}
strcpy(ms.Password, MakePassword(passwordIndex, 0));
ms.MissionsCompleted = mData->m->index + 1;
AutosaveAddMission(&gAutosave, &ms);
CArrayPushBack(&ms.MissionsCompleted, &mData->m->index);
ms.NextMission = mData->m->NextMission;
AutosaveAddCampaign(&gAutosave, &ms);
AutosaveSave(&gAutosave, GetConfigFilePath(AUTOSAVE_FILE));
}
@@ -706,19 +689,6 @@ static void MissionSummaryMenuDraw(
const int w = gGraphicsDevice.cachedConfig.Res.x;
const int h = gGraphicsDevice.cachedConfig.Res.y;
// Display password
if (strlen(gAutosave.LastMission.Password) > 0)
{
char s[64];
sprintf(s, "Last password: %s", gAutosave.LastMission.Password);
FontOpts opts = FontOptsNew();
opts.HAlign = ALIGN_CENTER;
opts.VAlign = ALIGN_END;
opts.Area = g->cachedConfig.Res;
opts.Pad.y = opts.Area.y / 12;
FontStrOpt(s, svec2i_zero(), opts);
}
// Display objectives and bonuses
struct vec2i pos = svec2i(w / 6, h / 2 + h / 10);
int idx = 1;
@@ -738,7 +708,7 @@ static void MissionSummaryMenuDraw(
s, "Objective %d: %d of %d, %d required", idx, o->done, o->Count,
o->Required);
FontOpts opts = FontOptsNew();
opts.Area = gGraphicsDevice.cachedConfig.Res;
opts.Area = g->cachedConfig.Res;
opts.Pad = pos;
if (!ObjectiveIsRequired(o))
{
@@ -750,7 +720,7 @@ static void MissionSummaryMenuDraw(
// Objective status text
opts = FontOptsNew();
opts.HAlign = ALIGN_END;
opts.Area = gGraphicsDevice.cachedConfig.Res;
opts.Area = g->cachedConfig.Res;
opts.Pad = pos;
if (!ObjectiveIsComplete(o))
{

View File

@@ -46,7 +46,7 @@ void CampaignEntryInit(CampaignEntry *entry, const char *title, GameMode mode)
CSTRDUP(entry->Info, title);
entry->Mode = mode;
}
void CampaignEntryCopy(CampaignEntry *dst, CampaignEntry *src)
void CampaignEntryCopy(CampaignEntry *dst, const CampaignEntry *src)
{
memcpy(dst, src, sizeof *dst);
if (src->Filename)

View File

@@ -2,7 +2,7 @@
C-Dogs SDL
A port of the legendary (and fun) action/arcade cdogs.
Copyright (c) 2013-2015, 2020 Cong Xu
Copyright (c) 2013-2015, 2020-2021 Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -40,7 +40,7 @@ typedef struct
} CampaignEntry;
void CampaignEntryInit(CampaignEntry *entry, const char *title, GameMode mode);
void CampaignEntryCopy(CampaignEntry *dst, CampaignEntry *src);
void CampaignEntryCopy(CampaignEntry *dst, const CampaignEntry *src);
bool CampaignEntryTryLoad(
CampaignEntry *entry, const char *path, GameMode mode);
void CampaignEntryTerminate(CampaignEntry *entry);

View File

@@ -87,7 +87,7 @@ bool IsAutoMapEnabled(const GameMode mode)
return mode != GAME_MODE_DOGFIGHT && mode != GAME_MODE_DEATHMATCH;
}
bool IsPasswordAllowed(const GameMode mode)
bool CanLevelSelect(const GameMode mode)
{
return mode == GAME_MODE_NORMAL;
}

View File

@@ -43,7 +43,7 @@ bool IsGameOptionsNeeded(const GameMode mode);
bool IsScoreNeeded(const GameMode mode);
bool HasObjectives(const GameMode mode);
bool IsAutoMapEnabled(const GameMode mode);
bool IsPasswordAllowed(const GameMode mode);
bool CanLevelSelect(const GameMode mode);
bool IsMissionBriefingNeeded(const GameMode mode, const char *missionBriefing);
bool AreKeysAllowed(const GameMode mode);
bool AreHealthPickupsAllowed(const GameMode mode);

View File

@@ -77,7 +77,7 @@ struct SongDef *gGameSongs = NULL;
struct SongDef *gMenuSongs = NULL;
bool CampaignLoad(Campaign *co, CampaignEntry *entry)
bool CampaignLoad(Campaign *co, const CampaignEntry *entry)
{
CASSERT(!co->IsLoaded, "loading campaign without unloading last one");
// Note: use the mode already set by the menus

View File

@@ -22,7 +22,7 @@
This file incorporates work covered by the following copyright and
permission notice:
Copyright (c) 2013-2014, Cong Xu
Copyright (c) 2013-2014, 2021 Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -67,7 +67,7 @@
extern struct MissionOptions gMission;
bool CampaignLoad(Campaign *co, CampaignEntry *entry);
bool CampaignLoad(Campaign *co, const CampaignEntry *entry);
void CampaignUnload(Campaign *co);
void MissionOptionsInit(struct MissionOptions *mo);

View File

@@ -958,25 +958,6 @@ static void PlaceKey(
CA_FOREACH_END()
}
}
static int CompareInts(const void *v1, const void *v2)
{
const int i1 = *(const int *)v1;
const int i2 = *(const int *)v2;
if (i1 > i2)
{
return -1;
}
else if (i1 < i2)
{
return 1;
}
return 0;
}
static bool IntsEqual(const void *v1, const void *v2)
{
return CompareInts(v1, v2) == 0;
}
static bool AllTilesAroundUnwalkable(
const MapBuilder *mb, const struct vec2i v)
{
@@ -1011,7 +992,7 @@ static void AddPillars(
// Sort and remove duplicates
qsort(
allChildren.data, allChildren.size, allChildren.elemSize,
CompareInts);
CompareIntsDesc);
CArrayUnique(&allChildren, IntsEqual);
}
CA_FOREACH_END()

View File

@@ -539,6 +539,40 @@ int Stricmp(const char *a, const char *b)
return ca - cb;
}
int CompareIntsAsc(const void *v1, const void *v2)
{
const int i1 = *(const int *)v1;
const int i2 = *(const int *)v2;
if (i1 < i2)
{
return -1;
}
else if (i1 > i2)
{
return 1;
}
return 0;
}
int CompareIntsDesc(const void *v1, const void *v2)
{
const int i1 = *(const int *)v1;
const int i2 = *(const int *)v2;
if (i1 > i2)
{
return -1;
}
else if (i1 < i2)
{
return 1;
}
return 0;
}
bool IntsEqual(const void *v1, const void *v2)
{
return *(const int *)v1 == *(const int *)v2;
}
BodyPart StrBodyPart(const char *s)
{
S2T(BODY_PART_HEAD, "head");

View File

@@ -188,6 +188,9 @@ char *Div8Str(int i);
void CamelToTitle(char *buf, const char *src);
bool StrEndsWith(const char *str, const char *suffix);
int Stricmp(const char *a, const char *b);
int CompareIntsAsc(const void *v1, const void *v2);
int CompareIntsDesc(const void *v1, const void *v2);
bool IntsEqual(const void *v1, const void *v2);
// Helper macros for defining type/str conversion funcs
#define T2S(_type, _str) \

View File

@@ -279,8 +279,8 @@ typedef struct
// so we can enable it if LAN servers are found
int MenuJoinIndex;
} CheckLANServerData;
static menu_t *MenuCreateContinue(const char *name, CampaignEntry *entry);
static menu_t *MenuCreateQuickPlay(const char *name, CampaignEntry *entry);
static menu_t *MenuCreateContinue(const char *name, const CampaignEntry *entry);
static menu_t *MenuCreateQuickPlay(const char *name, const CampaignEntry *entry);
static menu_t *MenuCreateCampaigns(
const char *name, const char *title, CampaignList *list,
const GameMode mode);
@@ -292,8 +292,9 @@ static menu_t *MenuCreateStart(
CustomCampaigns *campaigns)
{
menu_t *menu = MenuCreateNormal(name, "Start:", MENU_TYPE_NORMAL, 0);
const CampaignSave *cs = AutosaveGetLastCampaign(&gAutosave);
MenuAddSubmenu(
menu, MenuCreateContinue("Continue", &gAutosave.LastMission.Campaign));
menu, MenuCreateContinue("Continue", &cs->Campaign));
const int menuContinueIndex = (int)menu->u.normal.subMenus.size - 1;
MenuAddSubmenu(
menu, MenuCreateCampaigns(
@@ -317,9 +318,7 @@ static menu_t *MenuCreateStart(
MenuAddSubmenu(menu, MenuCreateSeparator(""));
MenuAddSubmenu(menu, MenuCreateBack("Back"));
if (strlen(gAutosave.LastMission.Password) == 0 ||
!gAutosave.LastMission.IsValid ||
strlen(gAutosave.LastMission.Campaign.Path) == 0)
if (!CampaignSaveIsValid(cs))
{
MenuDisableSubmenu(menu, menuContinueIndex);
}
@@ -334,12 +333,12 @@ static menu_t *MenuCreateStart(
typedef struct
{
GameMode GameMode;
CampaignEntry *Entry;
const CampaignEntry *Entry;
} StartGameModeData;
static void StartGameMode(menu_t *menu, void *data);
static menu_t *CreateStartGameMode(
const char *name, GameMode mode, CampaignEntry *entry)
const char *name, GameMode mode, const CampaignEntry *entry)
{
menu_t *menu = MenuCreate(name, MENU_TYPE_RETURN);
menu->enterSound = MENU_SOUND_START;
@@ -361,11 +360,11 @@ static void StartGameMode(menu_t *menu, void *data)
printf("Error: cannot load campaign %s\n", mData->Entry->Info);
}
}
static menu_t *MenuCreateContinue(const char *name, CampaignEntry *entry)
static menu_t *MenuCreateContinue(const char *name, const CampaignEntry *entry)
{
return CreateStartGameMode(name, GAME_MODE_NORMAL, entry);
}
static menu_t *MenuCreateQuickPlay(const char *name, CampaignEntry *entry)
static menu_t *MenuCreateQuickPlay(const char *name, const CampaignEntry *entry)
{
return CreateStartGameMode(name, GAME_MODE_QUICK_PLAY, entry);
}
@@ -429,17 +428,19 @@ static menu_t *MenuCreateCampaignItem(
// - Green for new campaigns
// - White (normal) for in-progress campaigns
// - Grey for complete campaigns
MissionSave m;
AutosaveLoadMission(&gAutosave, &m, entry->Path);
if (m.MissionsCompleted == entry->NumMissions)
const CampaignSave *m = AutosaveGetCampaign(&gAutosave, entry->Path);
if (m != NULL)
{
// Completed campaign
menu->color = colorGray;
}
else if (m.MissionsCompleted > 0)
{
// Campaign in progress
menu->color = colorYellow;
if ((int)m->MissionsCompleted.size == entry->NumMissions)
{
// Completed campaign
menu->color = colorGray;
}
else if (m->MissionsCompleted.size > 0)
{
// Campaign in progress
menu->color = colorYellow;
}
}
return menu;

View File

@@ -1,28 +1,7 @@
/*
C-Dogs SDL
A port of the legendary (and fun) action/arcade cdogs.
Copyright (C) 1995 Ronny Wester
Copyright (C) 2003 Jeremy Chin
Copyright (C) 2003-2007 Lucas Martin-King
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
This file incorporates work covered by the following copyright and
permission notice:
Copyright (c) 2013-2014, 2017 Cong Xu
Copyright (c) 2013-2014, 2017, 2021 Cong Xu
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -73,300 +52,51 @@
#define DONE "Done"
const char *MakePassword(int mission, int isTwoPlayers)
{
static char s[PASSWORD_MAX + 1];
int sum1, sum2, count;
size_t i, x;
static char *alphabet1 = "0123456789abcdefghijklmnopqrstuvwxyz";
static char *alphabet2 = "9876543210kjihgfedcbazyxwvutsrqponml";
char *alphabet = isTwoPlayers ? alphabet2 : alphabet1;
size_t base = strlen(alphabet);
sum1 = sum2 = 0;
for (i = 0; i < strlen(gCampaign.Setting.Title); i++)
{
sum1 += gCampaign.Setting.Title[i];
sum2 ^= gCampaign.Setting.Title[i];
}
const int seed = ConfigGetInt(&gConfig, "Game.RandomSeed");
x = ((sum2 << 23) | (mission << 16) | sum1) ^ (size_t)seed;
count = 0;
while (x > 0 && count < PASSWORD_MAX) {
i = x % base;
s[count++] = alphabet[i];
x /= base;
}
s[count] = 0;
return s;
}
static int TestPassword(const char *password)
{
int i;
for (i = 0; i < (int)gCampaign.Setting.Missions.size; i++)
{
// For legacy passwords, try both one and two players
if (strcmp(password, MakePassword(i, 0)) == 0 ||
strcmp(password, MakePassword(i, 1)) == 0)
{
return i;
}
}
return -1;
}
// Give user an alpha-num entry screen and returns the decoded mission number.
// If the password is invalid, the screen won't exit.
// If the user cancels, 0 is returned.
typedef struct
{
char Buffer[PASSWORD_MAX + 1];
int Mission;
int Selection;
} EnterCodeScreenData;
static void EnterCodeTerminate(GameLoopData *data);
static void EnterCodeScreenOnExit(GameLoopData *data);
static GameLoopResult EnterCodeScreenUpdate(GameLoopData *data, LoopRunner *l);
static void EnterCodeScreenDraw(GameLoopData *data);
static GameLoopData *EnterCodeScreen(const char *password)
{
EnterCodeScreenData *data;
CCALLOC(data, sizeof *data);
data->Selection = -1;
strcpy(data->Buffer, password);
return GameLoopDataNew(
data, EnterCodeTerminate, NULL, EnterCodeScreenOnExit,
NULL, EnterCodeScreenUpdate, EnterCodeScreenDraw);
}
static void EnterCodeTerminate(GameLoopData *data)
{
EnterCodeScreenData *eData = data->Data;
CFREE(eData);
}
static void EnterCodeScreenOnExit(GameLoopData *data)
{
const EnterCodeScreenData *eData = data->Data;
if (eData->Mission > 0)
{
gCampaign.MissionIndex = eData->Mission;
MenuPlaySound(MENU_SOUND_ENTER);
}
else
{
MenuPlaySound(MENU_SOUND_SWITCH);
}
}
#define PASSWORD_ENTRY_COLS 10
#define PASSWORD_LETTERS "abcdefghijklmnopqrstuvwxyz0123456789"
static bool PasswordEntry(EnterCodeScreenData *data, const int cmd);
static GameLoopResult EnterCodeScreenUpdate(GameLoopData *data, LoopRunner *l)
{
EnterCodeScreenData *eData = data->Data;
// Check if anyone pressed escape
int cmds[MAX_LOCAL_PLAYERS];
memset(cmds, 0, sizeof cmds);
GetPlayerCmds(&gEventHandlers, &cmds);
if (EventIsEscape(&gEventHandlers, cmds, GetMenuCmd(&gEventHandlers)))
{
eData->Mission = 0;
LoopRunnerPop(l);
return UPDATE_RESULT_OK;
}
const int cmd = GetMenuCmd(&gEventHandlers);
// Returning 1 means we are still entering password
if (PasswordEntry(eData, cmd))
{
return UPDATE_RESULT_DRAW;
}
if (strlen(eData->Buffer) == 0)
{
// Nothing in the password buffer; exit
eData->Mission = 0;
LoopRunnerPop(l);
return UPDATE_RESULT_OK;
}
eData->Mission = TestPassword(eData->Buffer);
if (eData->Mission == 0)
{
// Password doesn't work; play a bad sound
MenuPlaySound(MENU_SOUND_ERROR);
return UPDATE_RESULT_OK;
}
// Password works; exit
LoopRunnerPop(l);
return UPDATE_RESULT_OK;
}
static bool PasswordEntry(EnterCodeScreenData *data, const int cmd)
{
if (data->Selection == -1)
{
data->Selection = (int)strlen(PASSWORD_LETTERS);
}
if (cmd & CMD_BUTTON1)
{
if (data->Selection == (int)strlen(PASSWORD_LETTERS))
{
// Done
return false;
}
if (strlen(data->Buffer) < PASSWORD_MAX)
{
// enter a letter
data->Buffer[strlen(data->Buffer)] =
PASSWORD_LETTERS[data->Selection];
MenuPlaySound(MENU_SOUND_ENTER);
}
else
{
// Too many letters entered
MenuPlaySound(MENU_SOUND_ERROR);
}
}
else if (cmd & CMD_BUTTON2)
{
if (strlen(data->Buffer) > 0)
{
// Delete a letter
data->Buffer[strlen(data->Buffer) - 1] = 0;
MenuPlaySound(MENU_SOUND_SWITCH);
}
else
{
// No letters to delete
MenuPlaySound(MENU_SOUND_ERROR);
}
}
else if (cmd & CMD_LEFT)
{
if (data->Selection > 0)
{
data->Selection--;
MenuPlaySound(MENU_SOUND_SWITCH);
}
}
else if (cmd & CMD_RIGHT)
{
if (data->Selection < (int)strlen(PASSWORD_LETTERS))
{
data->Selection++;
MenuPlaySound(MENU_SOUND_SWITCH);
}
}
else if (cmd & CMD_UP)
{
if (data->Selection >= PASSWORD_ENTRY_COLS)
{
data->Selection -= PASSWORD_ENTRY_COLS;
MenuPlaySound(MENU_SOUND_SWITCH);
}
}
else if (cmd & CMD_DOWN)
{
if (data->Selection <=
(int)strlen(PASSWORD_LETTERS) - PASSWORD_ENTRY_COLS)
{
data->Selection += PASSWORD_ENTRY_COLS;
MenuPlaySound(MENU_SOUND_SWITCH);
}
}
return true;
}
static void EnterCodeScreenDraw(GameLoopData *data)
{
const EnterCodeScreenData *eData = data->Data;
BlitClearBuf(&gGraphicsDevice);
// Password display
struct vec2i pos = svec2i(
CenterX(FontStrW(eData->Buffer) + FontW('>') + FontW('<')),
gGraphicsDevice.cachedConfig.Res.y / 4);
pos = FontCh('>', pos);
pos = FontStr(eData->Buffer, pos);
FontCh('<', pos);
FontOpts opts = FontOptsNew();
opts.HAlign = ALIGN_CENTER;
opts.Area = gGraphicsDevice.cachedConfig.Res;
opts.Pad.y = gGraphicsDevice.cachedConfig.Res.y / 12;
FontStrOpt("Enter code", svec2i_zero(), opts);
// Draw password entry letters
#define ENTRY_SPACING 12
const int x =
CenterX(ENTRY_SPACING * (PASSWORD_ENTRY_COLS - 1) + FontW('a'));
const int y = (int)CenterY(
FontH() * ((strlen(PASSWORD_LETTERS) - 1) / PASSWORD_ENTRY_COLS));
for (int i = 0; i < (int)strlen(PASSWORD_LETTERS) + 1; i++)
{
pos = svec2i(
x + (i % PASSWORD_ENTRY_COLS) * ENTRY_SPACING,
y + (i / PASSWORD_ENTRY_COLS) * FontH());
color_t mask = (i == eData->Selection) ? colorRed : colorWhite;
if (i < (int)strlen(PASSWORD_LETTERS))
{
FontChMask(PASSWORD_LETTERS[i], pos, mask);
}
else
{
FontStrMask(DONE, pos, mask);
}
}
ShowControls();
BlitUpdateFromBuf(&gGraphicsDevice, gGraphicsDevice.screen);
}
typedef enum
{
RETURN_CODE_CONTINUE = -1,
RETURN_CODE_START = -2,
RETURN_CODE_ENTER_CODE = -3
} ReturnCode;
typedef struct
{
MenuSystem ms;
MissionSave save;
const CampaignSave *save;
int mission;
} PasswordData;
static void PasswordTerminate(GameLoopData *data);
static void PasswordOnEnter(GameLoopData *data);
static GameLoopResult PasswordUpdate(GameLoopData *data, LoopRunner *l);
static void PasswordDraw(GameLoopData *data);
} LevelSelectionData;
static void LevelSelectionTerminate(GameLoopData *data);
static void LevelSelectionOnEnter(GameLoopData *data);
static GameLoopResult LevelSelectionUpdate(GameLoopData *data, LoopRunner *l);
static void LevelSelectionDraw(GameLoopData *data);
static void MenuCreateStart(
MenuSystem *ms, const int mission, const MissionSave *save);
GameLoopData *EnterPassword(GraphicsDevice *graphics)
MenuSystem *ms, const int mission, const CampaignSave *save);
GameLoopData *LevelSelection(GraphicsDevice *graphics)
{
PasswordData *data;
CMALLOC(data, sizeof *data);
AutosaveLoadMission(&gAutosave, &data->save, gCampaign.Entry.Path);
LevelSelectionData *data;
CCALLOC(data, sizeof *data);
data->save = AutosaveGetCampaign(&gAutosave, gCampaign.Entry.Path);
MenuSystemInit(
&data->ms, &gEventHandlers, graphics, svec2i_zero(),
graphics->cachedConfig.Res);
data->mission = TestPassword(data->save.Password);
MenuCreateStart(&data->ms, data->mission, &data->save);
if (data->save)
{
data->mission = data->save->NextMission;
}
MenuCreateStart(&data->ms, data->mission, data->save);
return GameLoopDataNew(
data, PasswordTerminate, PasswordOnEnter, NULL,
NULL, PasswordUpdate, PasswordDraw);
data, LevelSelectionTerminate, LevelSelectionOnEnter, NULL,
NULL, LevelSelectionUpdate, LevelSelectionDraw);
}
static void MenuCreateLevelSelect(menu_t *levelSelect, const Campaign *co, const int i)
{
const Mission *m = CArrayGet(&co->Setting.Missions, i);
char buf[CDOGS_FILENAME_MAX];
sprintf(buf, "%d: %s", i + 1, m->Title);
menu_t *l = MenuCreateReturn(buf, i);
MenuAddSubmenu(levelSelect, l);
}
static void MenuCreateStart(
MenuSystem *ms, const int mission, const MissionSave *save)
MenuSystem *ms, const int mission, const CampaignSave *save)
{
ms->root = ms->current = MenuCreateNormal("", "", MENU_TYPE_NORMAL, 0);
@@ -378,44 +108,42 @@ static void MenuCreateStart(
// Create level select menus
menu_t *levelSelect = MenuCreateNormal(
"Level select...", "Select Level", MENU_TYPE_NORMAL, 0);
for (int i = 0;
i < MIN(save->MissionsCompleted + 1, (int)gCampaign.Setting.Missions.size);
i++)
if (save)
{
const Mission *m = CArrayGet(&gCampaign.Setting.Missions, i);
char buf[CDOGS_FILENAME_MAX];
sprintf(buf, "%d: %s", i + 1, m->Title);
menu_t *l = MenuCreateReturn(buf, i);
l->isDisabled = i > save->MissionsCompleted;
MenuAddSubmenu(levelSelect, l);
CA_FOREACH(const int, missionIndex, save->MissionsCompleted)
if (*missionIndex >= (int)gCampaign.Setting.Missions.size)
{
continue;
}
MenuCreateLevelSelect(levelSelect, &gCampaign, *missionIndex);
CA_FOREACH_END()
MenuCreateLevelSelect(levelSelect, &gCampaign, save->NextMission);
}
levelSelect->isDisabled = save->MissionsCompleted == 0;
levelSelect->isDisabled = save == NULL || save->MissionsCompleted.size == 0;
MenuAddSubmenu(ms->root, levelSelect);
MenuAddSubmenu(ms->root, MenuCreateReturn("Start campaign", RETURN_CODE_START));
MenuAddSubmenu(ms->root, MenuCreateReturn("Enter code...", RETURN_CODE_ENTER_CODE));
MenuAddExitType(ms, MENU_TYPE_RETURN);
}
static void PasswordTerminate(GameLoopData *data)
static void LevelSelectionTerminate(GameLoopData *data)
{
PasswordData *pData = data->Data;
LevelSelectionData *pData = data->Data;
MenuSystemTerminate(&pData->ms);
CFREE(data->Data);
}
static void PasswordOnEnter(GameLoopData *data)
static void LevelSelectionOnEnter(GameLoopData *data)
{
PasswordData *pData = data->Data;
LevelSelectionData *pData = data->Data;
// TODO: re-detect mission saves on enter
MenuReset(&pData->ms);
gCampaign.MissionIndex = 0;
}
static GameLoopResult PasswordUpdate(GameLoopData *data, LoopRunner *l)
static GameLoopResult LevelSelectionUpdate(GameLoopData *data, LoopRunner *l)
{
PasswordData *pData = data->Data;
LevelSelectionData *pData = data->Data;
const GameLoopResult result = MenuUpdate(&pData->ms);
if (result == UPDATE_RESULT_OK)
@@ -437,9 +165,6 @@ static GameLoopResult PasswordUpdate(GameLoopData *data, LoopRunner *l)
case RETURN_CODE_START:
LoopRunnerChange(l, GameOptions(gCampaign.Entry.Mode));
break;
case RETURN_CODE_ENTER_CODE:
LoopRunnerPush(l, EnterCodeScreen(pData->save.Password));
break;
default:
// Return code represents the mission to start on
CASSERT(returnCode >= 0, "Invalid return code for password menu");
@@ -451,9 +176,9 @@ static GameLoopResult PasswordUpdate(GameLoopData *data, LoopRunner *l)
}
return result;
}
static void PasswordDraw(GameLoopData *data)
static void LevelSelectionDraw(GameLoopData *data)
{
const PasswordData *pData = data->Data;
const LevelSelectionData *pData = data->Data;
MenuDraw(&pData->ms);
}

View File

@@ -53,4 +53,4 @@
#include "game_loop.h"
const char *MakePassword(int mission, int isTwoPlayers);
GameLoopData *EnterPassword(GraphicsDevice *graphics);
GameLoopData *LevelSelection(GraphicsDevice *graphics);

View File

@@ -448,9 +448,9 @@ static GameLoopResult PlayerSelectionUpdate(GameLoopData *data, LoopRunner *l)
if (isDone && hasAtLeastOneInput)
{
pData->waitResult = EVENT_WAIT_OK;
if (!gCampaign.IsClient && IsPasswordAllowed(gCampaign.Entry.Mode))
if (!gCampaign.IsClient && CanLevelSelect(gCampaign.Entry.Mode))
{
LoopRunnerChange(l, EnterPassword(&gGraphicsDevice));
LoopRunnerChange(l, LevelSelection(&gGraphicsDevice));
}
else
{

View File

@@ -61,22 +61,16 @@ PicManager gPicManager;
FEATURE(AutosaveInit, "Initialise autosave")
SCENARIO("Initialise autosave")
GIVEN("two autosaves")
Autosave autosave1, autosave2;
GIVEN("an autosave")
Autosave autosave;
WHEN("I initialise both")
AutosaveInit(&autosave1);
AutosaveInit(&autosave2);
WHEN("I initialise it")
AutosaveInit(&autosave);
THEN("their campaign entries should equal each other")
SHOULD_MEM_EQUAL(
&autosave1.LastMission.Campaign,
&autosave2.LastMission.Campaign,
sizeof autosave1.LastMission.Campaign);
AND("their passwords should equal each other")
SHOULD_STR_EQUAL(
autosave1.LastMission.Password,
autosave2.LastMission.Password);
THEN("the last campaign index should be -1")
SHOULD_INT_EQUAL(autosave.LastCampaignIndex, -1);
AND("the campaigns should be empty")
SHOULD_INT_EQUAL(autosave.Campaigns.size, 0);
SCENARIO_END
FEATURE_END
@@ -85,116 +79,107 @@ FEATURE(save_and_load, "Save and load")
GIVEN("an autosave with some values")
Autosave autosave1;
AutosaveInit(&autosave1);
MissionSave mission1;
memset(&mission1, 0, sizeof mission1);
CSTRDUP(mission1.Campaign.Path, "mission.cdogscpn");
strcpy(mission1.Password, "password");
AutosaveAddMission(&autosave1, &mission1);
CampaignSave cs1;
CampaignSaveInit(&cs1);
CSTRDUP(cs1.Campaign.Path, "campaign.cdogscpn");
cs1.NextMission = 1;
AutosaveAddCampaign(&autosave1, &cs1);
AND("I save it to file")
AutosaveSave(&autosave1, "tmp");
WHEN("I initialise and load a second autosave from that file")
Autosave autosave2;
AutosaveInit(&autosave2);
AutosaveLoad(&autosave2, "tmp");
THEN("their last mission paths should equal")
SHOULD_STR_EQUAL(
autosave2.LastMission.Campaign.Path,
autosave1.LastMission.Campaign.Path);
AND("their last mission passwords should equal")
SHOULD_STR_EQUAL(
autosave2.LastMission.Password,
autosave1.LastMission.Password);
AND("their mission paths should equal")
MissionSave mission2;
AutosaveLoadMission(&autosave2, &mission2, mission1.Campaign.Path);
SHOULD_STR_EQUAL(
mission2.Campaign.Path, mission1.Campaign.Path);
AND("their mission passwords should equal")
SHOULD_STR_EQUAL(mission2.Password, mission1.Password);
CampaignSave *cs2 = CArrayGet(&autosave2.Campaigns, 0);
THEN("their mission paths should equal")
SHOULD_STR_EQUAL(cs2->Campaign.Path, cs1.Campaign.Path);
AND("their next missions should equal")
SHOULD_STR_EQUAL(cs2->NextMission, cs1.NextMission);
SCENARIO_END
FEATURE_END
FEATURE(mission_autosaves, "Mission autosaves")
SCENARIO("Load non-existing mission autosave")
FEATURE(campaign_autosaves, "Campaign autosaves")
SCENARIO("Load non-existing campaign autosave")
GIVEN("an empty autosave")
Autosave autosave;
AutosaveInit(&autosave);
WHEN("I attempt to load a non-existing mission from it")
MissionSave mission;
AutosaveLoadMission(&autosave, &mission, "mission.cdogscpn");
const CampaignSave *cs = AutosaveGetCampaign(&autosave, "mission.cdogscpn");
THEN("the mission should be empty")
SHOULD_BE_TRUE(mission.Campaign.Path == NULL);
AND("the password should be empty")
SHOULD_STR_EQUAL(mission.Password, "");
THEN("the result should be null")
SHOULD_BE_TRUE(cs == NULL);
SCENARIO_END
SCENARIO("Add new mission autosave")
GIVEN("an autosave and a mission")
SCENARIO("Add new campaign autosave")
GIVEN("an autosave and a campaign")
Autosave autosave;
AutosaveInit(&autosave);
MissionSave mission1;
memset(&mission1, 0, sizeof mission1);
CSTRDUP(mission1.Campaign.Path, "mission.cdogscpn");
strcpy(mission1.Password, "password");
CampaignSave cs1;
CampaignSaveInit(&cs1);
CSTRDUP(cs1.Campaign.Path, "campaign.cdogscpn");
cs1.NextMission = 1;
WHEN("I add a new mission autosave to it")
AutosaveAddMission(&autosave, &mission1);
WHEN("I add a new campaign autosave to it")
AutosaveAddCampaign(&autosave, &cs1);
THEN("I should be able to find the mission in the autosave")
MissionSave mission2;
AutosaveLoadMission(&autosave, &mission2, mission1.Campaign.Path);
SHOULD_STR_EQUAL(mission2.Campaign.Path, mission1.Campaign.Path);
SHOULD_STR_EQUAL(mission2.Password, mission1.Password);
const CampaignSave *cs2 = AutosaveGetCampaign(&autosave, cs1.Campaign.Path);
SHOULD_STR_EQUAL(cs2->Campaign.Path, cs1.Campaign.Path);
SHOULD_INT_EQUAL(cs2->NextMission, cs1.NextMission);
SCENARIO_END
SCENARIO("Add existing mission autosave")
GIVEN("an autosave and a mission")
SCENARIO("Add existing campaign autosave")
GIVEN("an autosave and a campaign")
Autosave autosave;
AutosaveInit(&autosave);
MissionSave mission1;
memset(&mission1, 0, sizeof mission1);
CSTRDUP(mission1.Campaign.Path, "mission.cdogscpn");
strcpy(mission1.Password, "password");
mission1.MissionsCompleted = 3;
AND("I add the mission to the autosave")
AutosaveAddMission(&autosave, &mission1);
CampaignSave cs1;
CampaignSaveInit(&cs1);
CSTRDUP(cs1.Campaign.Path, "campaign.cdogscpn");
cs1.NextMission = 1;
int mission = 0;
CArrayPushBack(&cs1.MissionsCompleted, &mission);
AND("I add the campaign to the autosave")
AutosaveAddCampaign(&autosave, &cs1);
WHEN("I add the same campaign but with different next mission")
cs1.NextMission = 2;
AND("and differeiont missions completed")
CArrayClear(&cs1.MissionsCompleted);
mission = 1;
CArrayPushBack(&cs1.MissionsCompleted, &mission);
AutosaveAddCampaign(&autosave, &cs1);
WHEN("I add the same mission but with new password")
strcpy(mission1.Password, "new password");
AND("and less missions completed")
mission1.MissionsCompleted = 2;
AutosaveAddMission(&autosave, &mission1);
THEN("I should be able to find the mission in the autosave")
MissionSave mission2;
AutosaveLoadMission(&autosave, &mission2, mission1.Campaign.Path);
const CampaignSave *cs2 = AutosaveGetCampaign(&autosave, cs1.Campaign.Path);
AND("with the new details")
SHOULD_STR_EQUAL(mission2.Campaign.Path, mission1.Campaign.Path);
SHOULD_STR_EQUAL(mission2.Password, mission1.Password);
BUT("the greatest missions completed")
SHOULD_INT_EQUAL(mission2.MissionsCompleted, 3);
SHOULD_STR_EQUAL(cs2->Campaign.Path, cs1.Campaign.Path);
BUT("the greatest next mission")
SHOULD_INT_EQUAL(cs2->NextMission, 2);
AND("the union of completed missions")
SHOULD_INT_EQUAL(*(int *)CArrayGet(&cs2->MissionsCompleted, 0), 0);
SHOULD_INT_EQUAL(*(int *)CArrayGet(&cs2->MissionsCompleted, 1), 1);
SCENARIO_END
SCENARIO("Adding autosave updates last mission")
GIVEN("an autosave and a mission")
SCENARIO("Adding autosave updates last campaign")
GIVEN("an autosave and a campaign")
Autosave autosave;
AutosaveInit(&autosave);
MissionSave mission;
memset(&mission, 0, sizeof mission);
CSTRDUP(mission.Campaign.Path, "mission.cdogscpn");
strcpy(mission.Password, "password");
CampaignSave cs1;
CampaignSaveInit(&cs1);
CSTRDUP(cs1.Campaign.Path, "campaign.cdogscpn");
cs1.NextMission = 1;
WHEN("I add a new mission autosave to it")
AutosaveAddMission(&autosave, &mission);
WHEN("I add a new campaign autosave to it")
AutosaveAddCampaign(&autosave, &cs1);
THEN("the last mission will be the same as the new mission")
THEN("the last campaign will be the same")
const CampaignSave *cs2 = AutosaveGetLastCampaign(&autosave);
SHOULD_STR_EQUAL(
autosave.LastMission.Campaign.Path, mission.Campaign.Path);
SHOULD_STR_EQUAL(autosave.LastMission.Password, mission.Password);
cs2->Campaign.Path, cs1.Campaign.Path);
SHOULD_INT_EQUAL(cs2->NextMission, cs1.NextMission);
SCENARIO_END
FEATURE_END
@@ -202,5 +187,5 @@ CBEHAVE_RUN(
"Autosave features are:",
TEST_FEATURE(AutosaveInit),
TEST_FEATURE(save_and_load),
TEST_FEATURE(mission_autosaves)
TEST_FEATURE(campaign_autosaves)
)