Spellforce-Spell-framework
Loading...
Searching...
No Matches
sf_campaign_module.c
Go to the documentation of this file.
1
5
6#include <windows.h>
7#include <ctype.h>
8#include <stdio.h>
9#include <stdlib.h>
10#include <string.h>
11
12#include "sf_campaign_module.h"
13#include "sf_campaign_loader.h"
14#include "sf_hooks.h"
15#include "sf_wrappers.h"
16
19
21
25static bool campaign_map_exists(const char *campaign_folder, const char *map_name)
26{
27 char map_path[MAX_PATH];
28 snprintf(map_path, sizeof(map_path), "map\\%s\\%s.map", campaign_folder, map_name);
29
30 SF_String path;
31 uiAPI.SFStringConstructor_char(&path, map_path);
32 bool exists = (checkFileExists(&path) != 0);
33 uiAPI.SFStringDestructor(&path);
34
35 if (!exists)
36 {
37 log_debug(DEBUG_MED, "| - Campaign map not found: %s", map_path);
38 }
39
40 return exists;
41}
42
43/* Save folders the engine creates for itself in checkDirs(). A campaign that
44 * claimed one of these would share its saves with a vanilla campaign. */
45static const char *k_reserved_folders[] = { "save", "temp", "char", "campaign2", "campaign3" };
46
47static bool equals_ignore_case(const char *a, const char *b)
48{
49 while (*a != '\0' && *b != '\0')
50 {
51 if (tolower((unsigned char)*a) != tolower((unsigned char)*b))
52 {
53 return false;
54 }
55 a++;
56 b++;
57 }
58 return *a == *b;
59}
60
65static bool is_campaign_folder_safe(const char *folder, const char *source)
66{
67 if (strchr(folder, '\\') != NULL || strchr(folder, '/') != NULL ||
68 strchr(folder, ':') != NULL || strstr(folder, "..") != NULL)
69 {
71 "Campaign [%s]: campaign_folder \"%s\" must be a plain folder name (no slashes, colons or \"..\")",
72 source, folder);
73 return false;
74 }
75
76 for (size_t i = 0; i < (sizeof(k_reserved_folders) / sizeof(k_reserved_folders[0])); i++)
77 {
78 if (equals_ignore_case(folder, k_reserved_folders[i]))
79 {
81 "Campaign [%s]: campaign_folder \"%s\" is reserved by the engine",
82 source, folder);
83 return false;
84 }
85 }
86
87 return true;
88}
89
94static bool validate_campaign(CampaignJson *campaign, const char *source)
95{
96 SFSF_CampaignDef *def = &campaign->def;
97 bool valid = true;
98
99 if (!campaign->found_name || def->name[0] == '\0')
100 {
101 report_mod_error(g_campaign_mod, "Campaign [%s]: missing required field \"name\"", source);
102 valid = false;
103 }
104
105 if (!campaign->found_start_map || def->start_map[0] == '\0')
106 {
107 report_mod_error(g_campaign_mod, "Campaign [%s]: missing required field \"start_map\"", source);
108 valid = false;
109 }
110
111 if (!campaign->found_campaign_folder || def->campaign_folder[0] == '\0')
112 {
113 report_mod_error(g_campaign_mod, "Campaign [%s]: missing required field \"campaign_folder\"", source);
114 valid = false;
115 }
116 else if (!is_campaign_folder_safe(def->campaign_folder, source))
117 {
118 valid = false;
119 }
120
121 if (campaign->found_avatar_type && def->avatar_type != -1 &&
123 {
125 "Campaign [%s]: avatar_type %u is outside the custom range %u-%u",
126 source, def->avatar_type,
127 (uint32_t)SFSF_AVATAR_TYPE_BASE, (uint32_t)SFSF_AVATAR_TYPE_MAX);
128 valid = false;
129 }
130
131 if (!valid)
132 {
133 // The map checks below need a usable folder and map name to build a path.
134 return false;
135 }
136
137 /* The fields are well formed - now make sure they point at maps that are
138 * actually on disk, so a typo is caught here rather than at load time. */
139 if (!campaign_map_exists(def->campaign_folder, def->start_map))
140 {
142 "Campaign [%s]: start_map \"%s\" not found at map\\%s\\%s.map",
143 source, def->start_map, def->campaign_folder, def->start_map);
144 valid = false;
145 }
146
147 if (def->tutorial_map[0] != '\0' &&
148 !campaign_map_exists(def->campaign_folder, def->tutorial_map))
149 {
151 "Campaign [%s]: tutorial_map \"%s\" not found at map\\%s\\%s.map",
152 source, def->tutorial_map, def->campaign_folder, def->tutorial_map);
153 valid = false;
154 }
155
156 if (!valid)
157 {
158 return false;
159 }
160
161 /* Advisory only - the campaign still loads. */
162 if (def->description[0] == '\0')
163 {
164 report_mod_warning(g_campaign_mod, "Campaign [%s]: no description set", source);
165 }
166
167 if (def->author[0] == '\0')
168 {
169 report_mod_warning(g_campaign_mod, "Campaign [%s]: no author set", source);
170 }
171
172 if (def->tutorial_map[0] == '\0')
173 {
174 log_debug(DEBUG_MED, "| - Campaign [%s]: no tutorial_map, the tutorial checkbox will be ignored", source);
175 }
176
177 return true;
178}
179
184static bool check_campaign_conflicts(const CampaignJson *campaign, const char *source)
185{
186 const SFSF_CampaignDef *def = &campaign->def;
187 bool conflict_free = true;
188
190 "Campaign folder"))
191 {
192 // Two campaigns sharing a save folder would overwrite each other's saves.
193 log_error("| - Campaign [%s] cannot share campaign_folder \"%s\"", source, def->campaign_folder);
194 conflict_free = false;
195 }
196
197 if (!claim_string_id(CONFLICT_CAMPAIGN_NAME, def->name, g_campaign_mod, "Campaign name"))
198 {
199 // Not fatal on its own, but the menu buttons become indistinguishable.
200 log_warning("| - Campaign [%s] reuses the name \"%s\"", source, def->name);
201 }
202
203 if (def->avatar_type != -1 &&
205 0, "Campaign avatar type"))
206 {
207 log_error("| - Campaign [%s] cannot share avatar_type %u", source, def->avatar_type);
208 conflict_free = false;
209 }
210
211 return conflict_free;
212}
213
215static bool load_campaign_file(const char *path, const char *filename)
216{
218 {
220 "Campaign [%s] skipped: the registry is full (%d campaigns max)",
221 filename, SFSF_MAX_CAMPAIGNS);
222 return false;
223 }
224
225 CampaignJson campaign;
226 if (!parse_campaign_json_file(path, &campaign))
227 {
228 report_mod_error(g_campaign_mod, "Campaign [%s] could not be parsed", filename);
229 return false;
230 }
231
232 if (!validate_campaign(&campaign, filename))
233 {
234 return false;
235 }
236
237 if (!check_campaign_conflicts(&campaign, filename))
238 {
239 return false;
240 }
241
242 int32_t index = register_campaign(&campaign.def);
243 if (index < 0)
244 {
245 report_mod_error(g_campaign_mod, "Campaign [%s] was rejected by the registry", filename);
246 return false;
247 }
248
249 if (campaign.def.avatar_type == -1)
250 {
252 g_campaign_mod, 0, "Campaign avatar type");
253 }
254
255 return true;
256}
257
259{
260 char version_tag_buffer[128];
261 snprintf(version_tag_buffer, sizeof(version_tag_buffer), "%d.%d.%d-%s",
264
265 g_campaign_mod = createModInfo("Custom Campaign Module", version_tag_buffer,
266 "Muddykat, UnSchtalch",
267 "Loads custom campaigns defined by JSON files in the sfsf\\campaigns folder.");
270
271 char currentDir[MAX_PATH];
272 GetCurrentDirectory(MAX_PATH, currentDir);
273
274 char campaignDirectory[MAX_PATH];
275 snprintf(campaignDirectory, sizeof(campaignDirectory), "%s\\sfsf\\campaigns", currentDir);
276
277 char searchPath[MAX_PATH];
278 snprintf(searchPath, sizeof(searchPath), "%s\\*.json", campaignDirectory);
279
280 WIN32_FIND_DATA findFileData;
281 HANDLE hFind = FindFirstFile(searchPath, &findFileData);
282 if (hFind == INVALID_HANDLE_VALUE)
283 {
284 // No folder and no campaigns is a perfectly normal install.
285 log_info("| - No campaigns found in %s (custom campaigns disabled)", campaignDirectory);
286 return;
287 }
288
289 uint32_t loaded = 0;
290 uint32_t skipped = 0;
291 do
292 {
293 if ((findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
294 {
295 continue;
296 }
297
298 char campaignPath[MAX_PATH];
299 snprintf(campaignPath, sizeof(campaignPath), "%s\\%s", campaignDirectory, findFileData.cFileName);
300
301 log_info("| - Reading campaign definition [%s]", findFileData.cFileName);
302
303 if (load_campaign_file(campaignPath, findFileData.cFileName))
304 {
305 loaded++;
306 }
307 else
308 {
309 skipped++;
310 }
311 } while (FindNextFile(hFind, &findFileData) != 0);
312
313 FindClose(hFind);
314
315 log_info("| - %u campaign(s) registered, %u skipped", loaded, skipped);
316}
317
uint32_t g_campaign_count
int32_t register_campaign(const SFSF_CampaignDef *def)
#define SFSF_AVATAR_TYPE_BASE
SFSF_CampaignDef g_campaigns[16]
#define SFSF_AVATAR_TYPE_MAX
checkFileExists_ptr checkFileExists
#define SFSF_MAX_CAMPAIGNS
bool parse_campaign_json_file(const char *path, CampaignJson *out_campaign)
Reads and parses a campaign JSON file.
void initialize_campaign_module()
Creates the module's mod entry and loads sfsf\campaigns*.json.
SFMod * g_campaign_mod
bool claim_numeric_id(ConflictDomain domain, uint32_t id, SFMod *mod, uint32_t vanilla_max, const char *id_label)
Claims a numeric ID within a domain for a mod.
void clear_mod_errors(SFMod *mod)
Empties a mod's error buffer.
void register_mod_for_listing(SFMod *mod, ModType type)
Adds a mod to the list rendered by the in-game mod list screen.
bool claim_string_id(ConflictDomain domain, const char *key, SFMod *mod, const char *id_label)
Claims a string key within a domain for a mod. Comparison is case-insensitive, since the keys are Win...
void report_mod_warning(SFMod *mod, const char *format,...)
Logs a warning against a mod at DEBUG_INFO, i.e. always shown.
void report_mod_error(SFMod *mod, const char *format,...)
Logs an error, appends it to the mod's error buffer and counts it.
@ CONFLICT_CAMPAIGN_FOLDER
@ CONFLICT_CAMPAIGN_AVATAR_TYPE
@ CONFLICT_CAMPAIGN_NAME
@ MOD_TYPE_CORE
UiFunctions uiAPI
Definition sf_hooks.c:50
void log_error(const char *format,...)
void log_warning(const char *format,...)
Logs a warning at DEBUG_INFO, i.e. always shown.
void log_debug(DebugLevel level, const char *format,...)
void log_info(const char *format,...)
SFMod * createModInfo(const char *mod_id, const char *mod_version, const char *mod_author, const char *mod_description)
#define SPELLFRAMEWORK_TAG
Definition sf_registry.h:3
#define SPELLFRAMEWORK_VERSION_PATCH
Definition sfsf.h:20
#define SPELLFRAMEWORK_VERSION_MAJOR
Definition sfsf.h:18
#define SPELLFRAMEWORK_VERSION_MINOR
Definition sfsf.h:19
Parse result: the definition plus which keys were actually present.
SFSF_CampaignDef def