Skip to content

Commit 00a96fe

Browse files
TWCMmaxjivi05Xnick417x
authored
feat: steam dlc manifest gen , game language gui for steam game , Download for import game (#580)
* init for dlc manifeast generation for steam game in steam launcher * remove unnessary changes * add back the storage check * bug fix for installscript removal * restore containerLanguage to default to english * From bd72454488e6a1763ee15b2a5359110005441fce Mon Sep 17 00:00:00 2001 From: Xnick417x <[email protected]> Date: Sat, 27 Jun 2026 05:43:04 +0000 Subject: [PATCH] Fix Steam DLC manifest generation and restore install storage gate - Restore the free-space precondition on the Steam/Epic/GOG install dialogs; allow a Steam download when the game is already installed - Compute SizeOnDisk recursively and null-guard the directory listing so a listing error no longer drops all depot data - Escape name/installdir/language/install-script fields in the native ACF writer and double backslashes first in the Kotlin manifest escaper - Size the depot/shared/install-script buffers to the input length to stop silent truncation for games with many depots - Exclude shared depots from InstalledDepots and drop the dead depot-id-in-DLC inclusion clause; classify depots the same on both manifest paths - Restore the thread interrupt flag and guard per-DLC depot resolution - Hide the duplicate Simplified Chinese language option - Add Game Language strings to all locales - Rebuild steam.exe * add back the section label, which i removed accidentally in previous merge commit * move the game language setting from setting>Other to setting > Store > Steam * Fix Steam/Epic install storage gate and DLC manifest generation - Gate install on newly-selected DLC size for already-installed games instead of skipping the free-space check entirely (Steam and Epic). - Refresh the container appmanifest on every launch so newly installed DLC and language changes propagate. - Emit dlcappid in the native-launcher manifest the same way the generated manifest does. - Pass the resolved game language into getDownloadableDepots so depot filtering matches the language written to the manifest. - Deduplicate calculated DLC app IDs and drop unrelated whitespace churn. --------- Co-authored-by: MaxsTechReview <[email protected]> Co-authored-by: Xnick417x <[email protected]>
1 parent 4d3ea2f commit 00a96fe

24 files changed

Lines changed: 586 additions & 55 deletions

File tree

app/src/main/app/shell/UnifiedActivity.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7683,7 +7683,9 @@ class UnifiedActivity :
76837683
} catch (e: Exception) {
76847684
0L
76857685
}
7686-
val isInstallEnabled = installed || totalInstallSize == 0L || availableBytes >= totalInstallSize
7686+
// Installed game: base content is on disk, so only gate on the newly-selected DLC bytes.
7687+
val requiredBytes = if (installed) selectedDlcInstallBytes else totalInstallSize
7688+
val isInstallEnabled = requiredBytes == 0L || availableBytes >= requiredBytes
76877689
val installActionEnabled = isInstallEnabled && !hasBlockingEpicDownload
76887690
val installPathDisplay = if (installed) app.installPath else (customPath ?: EpicConstants.defaultEpicGamesPath(context))
76897691

@@ -9954,6 +9956,7 @@ class UnifiedActivity :
99549956
val context = LocalContext.current
99559957
var isLoading by remember { mutableStateOf(true) }
99569958
var selectedManifestSizes by remember { mutableStateOf(SteamService.ManifestSizes()) }
9959+
var baseInstallSize by remember(app.id) { mutableStateOf(0L) }
99579960
var dlcApps by remember { mutableStateOf<List<SteamApp>>(emptyList()) }
99589961
var dlcSizes by remember { mutableStateOf<Map<Int, SteamService.ManifestSizes>>(emptyMap()) }
99599962
var installedDlcIds by remember(app.id) { mutableStateOf<Set<Int>>(emptySet()) }
@@ -10019,6 +10022,7 @@ class UnifiedActivity :
1001910022
installedDlcIds = loadData.installedDlcIds
1002010023
selectedDlcIds.removeAll(loadData.installedDlcIds)
1002110024
selectedManifestSizes = loadData.baseManifestSizes
10025+
baseInstallSize = loadData.baseManifestSizes.installSize
1002210026
installed = loadData.installed
1002310027
isLoading = false
1002410028
}
@@ -10046,7 +10050,11 @@ class UnifiedActivity :
1004610050
} catch (e: Exception) {
1004710051
0L
1004810052
}
10049-
val isInstallEnabled = totalInstallSize == 0L || availableBytes >= totalInstallSize
10053+
// For an already-installed game the base content is on disk, so only require free
10054+
// space for the newly-selected DLC (already-installed DLC is excluded from selection).
10055+
val requiredBytes =
10056+
if (installed == true) (totalInstallSize - baseInstallSize).coerceAtLeast(0L) else totalInstallSize
10057+
val isInstallEnabled = requiredBytes == 0L || availableBytes >= requiredBytes
1005010058
val installPathDisplay = customPath ?: SteamService.defaultAppInstallPath
1005110059

1005210060
val dlcItems =
-19 KB
Binary file not shown.

app/src/main/cpp/wn-steam-launcher/src/main.cpp

Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,24 @@ static void stage_steam_config(void) {
231231
}
232232
}
233233

234+
// Escape a free-text value for a VDF/ACF quoted field: double backslashes, then
235+
// escape quotes and newlines. Mirrors the Kotlin escapeString() so the C++ and
236+
// Kotlin manifest paths produce identical, well-formed output.
237+
static std::string vdf_escape(const char* s) {
238+
std::string out;
239+
if (!s) return out;
240+
for (const char* p = s; *p; ++p) {
241+
switch (*p) {
242+
case '\\': out += "\\\\"; break;
243+
case '"': out += "\\\""; break;
244+
case '\n': out += "\\n"; break;
245+
case '\r': out += "\\r"; break;
246+
default: out += *p; break;
247+
}
248+
}
249+
return out;
250+
}
251+
234252
static void stage_app_manifest(uint32_t appId, const char* gameExe) {
235253
if (appId == 0 || !gameExe) return;
236254
const char* marker = "\\steamapps\\common\\";
@@ -258,33 +276,155 @@ static void stage_app_manifest(uint32_t appId, const char* gameExe) {
258276
snprintf(acf, sizeof(acf),
259277
"C:\\Program Files (x86)\\Steam\\steamapps\\appmanifest_%u.acf",
260278
appId);
279+
const char* owner = getenv("WN_STEAM_STEAMID");
280+
const char* depotsEnv = getenv("WN_STEAM_DEPOTS");
281+
const char* sharedEnv = getenv("WN_STEAM_SHARED_DEPOTS");
282+
const char* appName = getenv("WN_STEAM_APP_NAME");
283+
const char* installScriptsEnv = getenv("WN_STEAM_INSTALL_SCRIPTS");
284+
const char* language = getenv("WN_STEAM_LANGUAGE");
285+
const char* buildIdStr = getenv("WN_STEAM_BUILD_ID");
286+
const char* sizeOnDiskStr = getenv("WN_STEAM_SIZE_ON_DISK");
287+
const char* bytesToDownloadStr = getenv("WN_STEAM_BYTES_TO_DOWNLOAD");
288+
const char* bytesToStageStr = getenv("WN_STEAM_BYTES_TO_STAGE");
289+
if (!appName || !*appName) appName = installdir;
290+
if (!language || !*language) language = "english";
291+
unsigned long long buildId = (buildIdStr && *buildIdStr) ? strtoull(buildIdStr, NULL, 10) : 0ULL;
292+
unsigned long long sizeOnDisk = (sizeOnDiskStr && *sizeOnDiskStr) ? strtoull(sizeOnDiskStr, NULL, 10) : 0ULL;
293+
unsigned long long bytesToDownload = (bytesToDownloadStr && *bytesToDownloadStr) ? strtoull(bytesToDownloadStr, NULL, 10) : 0ULL;
294+
unsigned long long bytesToStage = (bytesToStageStr && *bytesToStageStr) ? strtoull(bytesToStageStr, NULL, 10) : 0ULL;
261295
FILE* f = fopen(acf, "w");
262296
if (!f) {
263297
log_line("[wn-launcher] app manifest: fopen(%s) failed", acf);
264298
return;
265299
}
266-
const char* owner = getenv("WN_STEAM_STEAMID");
300+
std::string nameEsc = vdf_escape(appName);
301+
std::string installdirEsc = vdf_escape(installdir);
302+
std::string languageEsc = vdf_escape(language);
267303
fprintf(f,
268304
"\"AppState\"\n"
269305
"{\n"
270306
"\t\"appid\"\t\t\"%u\"\n"
271-
"\t\"Universe\"\t\t\"1\"\n"
307+
"\t\"universe\"\t\t\"1\"\n"
308+
"\t\"LauncherPath\"\t\t\"C:\\\\Program Files (x86)\\\\Steam\\\\steam.exe\"\n"
272309
"\t\"name\"\t\t\"%s\"\n"
273310
"\t\"StateFlags\"\t\t\"4\"\n"
274311
"\t\"installdir\"\t\t\"%s\"\n"
275-
"\t\"LastUpdated\"\t\t\"0\"\n"
276-
"\t\"SizeOnDisk\"\t\t\"0\"\n"
277-
"\t\"buildid\"\t\t\"0\"\n"
312+
"\t\"LastUpdated\"\t\t\"%llu\"\n"
313+
"\t\"LastPlayed\"\t\t\"0\"\n"
314+
"\t\"SizeOnDisk\"\t\t\"%llu\"\n"
315+
"\t\"StagingSize\"\t\t\"0\"\n"
316+
"\t\"buildid\"\t\t\"%llu\"\n"
278317
"\t\"LastOwner\"\t\t\"%s\"\n"
279-
"\t\"InstalledDepots\"\n"
318+
"\t\"DownloadType\"\t\t\"1\"\n"
319+
"\t\"UpdateResult\"\t\t\"0\"\n"
320+
"\t\"BytesToDownload\"\t\t\"%llu\"\n"
321+
"\t\"BytesDownloaded\"\t\t\"%llu\"\n"
322+
"\t\"BytesToStage\"\t\t\"%llu\"\n"
323+
"\t\"BytesStaged\"\t\t\"%llu\"\n"
324+
"\t\"TargetBuildID\"\t\t\"%llu\"\n"
325+
"\t\"AutoUpdateBehavior\"\t\t\"0\"\n"
326+
"\t\"AllowOtherDownloadsWhileRunning\"\t\t\"0\"\n"
327+
"\t\"ScheduledAutoUpdate\"\t\t\"0\"\n",
328+
appId, nameEsc.c_str(), installdirEsc.c_str(),
329+
(unsigned long long)time(NULL),
330+
sizeOnDisk, buildId,
331+
(owner && *owner) ? owner : "0",
332+
bytesToDownload, bytesToDownload,
333+
bytesToStage, bytesToStage, buildId);
334+
// Write InstalledDepots with depot data from WN_STEAM_DEPOTS env var.
335+
// Format: depotId:manifestGid:size[:dlcAppId],...
336+
if (depotsEnv && *depotsEnv) {
337+
fprintf(f, "\t\"InstalledDepots\"\n\t{\n");
338+
std::vector<char> buf(strlen(depotsEnv) + 1);
339+
memcpy(buf.data(), depotsEnv, buf.size());
340+
char* token = strtok(buf.data(), ",");
341+
while (token) {
342+
// Parse depotId:manifestGid:size[:dlcAppId]
343+
char* colon1 = strchr(token, ':');
344+
if (!colon1) { token = strtok(NULL, ","); continue; }
345+
*colon1 = '\0';
346+
const char* depotIdStr = token;
347+
char* manifestStart = colon1 + 1;
348+
char* colon2 = strchr(manifestStart, ':');
349+
if (!colon2) { token = strtok(NULL, ","); continue; }
350+
*colon2 = '\0';
351+
const char* manifestStr = manifestStart;
352+
char* sizeStart = colon2 + 1;
353+
char* colon3 = strchr(sizeStart, ':');
354+
const char* sizeStr, *dlcAppIdStr;
355+
if (colon3) {
356+
*colon3 = '\0';
357+
sizeStr = sizeStart;
358+
dlcAppIdStr = colon3 + 1;
359+
} else {
360+
sizeStr = sizeStart;
361+
dlcAppIdStr = NULL;
362+
}
363+
fprintf(f, "\t\t\"%s\"\n\t\t{\n"
364+
"\t\t\t\"manifest\"\t\t\"%s\"\n"
365+
"\t\t\t\"size\"\t\t\"%s\"\n",
366+
depotIdStr, manifestStr, sizeStr);
367+
if (dlcAppIdStr && *dlcAppIdStr) {
368+
fprintf(f, "\t\t\t\"dlcappid\"\t\t\"%s\"\n", dlcAppIdStr);
369+
}
370+
fprintf(f, "\t\t}\n");
371+
token = strtok(NULL, ",");
372+
}
373+
fprintf(f, "\t}\n");
374+
} else {
375+
fprintf(f, "\t\"InstalledDepots\"\n\t{\n\t}\n");
376+
}
377+
// Write InstallScripts from WN_STEAM_INSTALL_SCRIPTS env var.
378+
// Format: depotId:scriptFilename,...
379+
if (installScriptsEnv && *installScriptsEnv) {
380+
fprintf(f, "\t\"InstallScripts\"\n\t{\n");
381+
std::vector<char> isbuf(strlen(installScriptsEnv) + 1);
382+
memcpy(isbuf.data(), installScriptsEnv, isbuf.size());
383+
char* istoken = strtok(isbuf.data(), ",");
384+
while (istoken) {
385+
char* iscolon = strchr(istoken, ':');
386+
if (!iscolon) { istoken = strtok(NULL, ","); continue; }
387+
*iscolon = '\0';
388+
std::string scriptEsc = vdf_escape(iscolon + 1);
389+
fprintf(f, "\t\t\"%s\"\t\t\"%s\"\n", istoken, scriptEsc.c_str());
390+
istoken = strtok(NULL, ",");
391+
}
392+
fprintf(f, "\t}\n");
393+
}
394+
// Write SharedDepots from WN_STEAM_SHARED_DEPOTS env var.
395+
// Format: sourceDepotId:targetAppId,...
396+
if (sharedEnv && *sharedEnv) {
397+
fprintf(f, "\t\"SharedDepots\"\n\t{\n");
398+
std::vector<char> sbuf(strlen(sharedEnv) + 1);
399+
memcpy(sbuf.data(), sharedEnv, sbuf.size());
400+
char* stoken = strtok(sbuf.data(), ",");
401+
while (stoken) {
402+
char* scolon = strchr(stoken, ':');
403+
if (!scolon) { stoken = strtok(NULL, ","); continue; }
404+
*scolon = '\0';
405+
fprintf(f, "\t\t\"%s\"\t\t\"%s\"\n", stoken, scolon + 1);
406+
stoken = strtok(NULL, ",");
407+
}
408+
fprintf(f, "\t}\n");
409+
}
410+
fprintf(f,
411+
"\t\"UserConfig\"\n"
412+
"\t{\n"
413+
"\t\t\"language\"\t\t\"%s\"\n"
414+
"\t}\n"
415+
"\t\"MountedConfig\"\n"
280416
"\t{\n"
417+
"\t\t\"language\"\t\t\"%s\"\n"
281418
"\t}\n"
282419
"}\n",
283-
appId, installdir, installdir,
284-
(owner && *owner) ? owner : "0");
420+
languageEsc.c_str(), languageEsc.c_str());
285421
fclose(f);
286-
log_line("[wn-launcher] app manifest staged: %s (installdir=\"%s\", StateFlags=4)",
287-
acf, installdir);
422+
log_line("[wn-launcher] app manifest staged: %s (installdir=\"%s\", "
423+
"depots=%s shared=%s scripts=%s)",
424+
acf, installdir,
425+
depotsEnv && *depotsEnv ? depotsEnv : "(none)",
426+
sharedEnv && *sharedEnv ? sharedEnv : "(none)",
427+
installScriptsEnv && *installScriptsEnv ? installScriptsEnv : "(none)");
288428
}
289429

290430
// Counts running game processes (matches LaunchApp's canonical name or the literal

app/src/main/feature/settings/stores/StoresFragment.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.winlator.cmod.feature.stores.gog.service.GOGAuthManager
2525
import com.winlator.cmod.feature.stores.gog.service.GOGService
2626
import com.winlator.cmod.feature.stores.gog.ui.auth.GOGOAuthActivity
2727
import com.winlator.cmod.feature.stores.steam.SteamLoginActivity
28+
import com.winlator.cmod.feature.stores.steam.enums.Language
2829
import com.winlator.cmod.feature.stores.steam.service.SteamService
2930
import com.winlator.cmod.feature.stores.steam.utils.PrefManager
3031
import com.winlator.cmod.shared.android.DirectoryPickerDialog
@@ -150,6 +151,11 @@ class StoresFragment : Fragment() {
150151
onPickSteamFolder = { pickFolder(PrefManager.steamDownloadFolder) { PrefManager.steamDownloadFolder = it } },
151152
onPickEpicFolder = { pickFolder(PrefManager.epicDownloadFolder) { PrefManager.epicDownloadFolder = it } },
152153
onPickGogFolder = { pickFolder(PrefManager.gogDownloadFolder) { PrefManager.gogDownloadFolder = it } },
154+
onContainerLanguageSelected = { index ->
155+
val langName = Language.containerLangForIndex(index)
156+
PrefManager.containerLanguage = langName
157+
refresh()
158+
},
153159
bridge = (requireActivity() as? UnifiedActivity)?.settingsNavBridge,
154160
)
155161
}
@@ -165,6 +171,8 @@ class StoresFragment : Fragment() {
165171
// Helpers
166172
private fun refresh() {
167173
val ctx = context ?: return
174+
val containerLanguageLabels = Language.displayLabels()
175+
val containerLanguageIndex = Language.indexForContainerLang(PrefManager.containerLanguage)
168176
storeState =
169177
StoreState(
170178
isSteamLoggedIn = SteamService.isLoggedIn,
@@ -178,6 +186,8 @@ class StoresFragment : Fragment() {
178186
steamFolder = resolveUri(PrefManager.steamDownloadFolder, ctx),
179187
epicFolder = resolveUri(PrefManager.epicDownloadFolder, ctx),
180188
gogFolder = resolveUri(PrefManager.gogDownloadFolder, ctx),
189+
containerLanguageLabels = containerLanguageLabels,
190+
containerLanguageIndex = containerLanguageIndex,
181191
)
182192
}
183193

app/src/main/feature/settings/stores/StoresScreen.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import androidx.compose.material.icons.outlined.Folder
4444
import androidx.compose.material.icons.outlined.FolderShared
4545
import androidx.compose.material.icons.outlined.Gamepad
4646
import androidx.compose.material.icons.outlined.KeyboardArrowDown
47+
import androidx.compose.material.icons.outlined.Language
4748
import androidx.compose.material.icons.outlined.Public
4849
import androidx.compose.material.icons.outlined.Speed
4950
import androidx.compose.material.icons.outlined.Wifi
@@ -109,6 +110,8 @@ data class StoreState(
109110
val steamFolder: String = "",
110111
val epicFolder: String = "",
111112
val gogFolder: String = "",
113+
val containerLanguageLabels: List<String> = emptyList(),
114+
val containerLanguageIndex: Int = 0,
112115
)
113116

114117
@Composable
@@ -128,6 +131,7 @@ fun StoresScreen(
128131
onPickSteamFolder: () -> Unit,
129132
onPickEpicFolder: () -> Unit,
130133
onPickGogFolder: () -> Unit,
134+
onContainerLanguageSelected: (Int) -> Unit,
131135
bridge: SettingsNavBridge? = null,
132136
) {
133137
val layoutDirection = LocalLayoutDirection.current
@@ -258,6 +262,18 @@ fun StoresScreen(
258262
onOptionSelected = onDownloadServerChanged,
259263
)
260264

265+
val gameLanguageOptions = remember(state.containerLanguageLabels) {
266+
state.containerLanguageLabels.mapIndexed { index, label -> index to label }
267+
}
268+
SettingsDropdownCard(
269+
title = stringResource(R.string.settings_other_game_language_title),
270+
subtitle = stringResource(R.string.settings_other_game_language_summary),
271+
icon = Icons.Outlined.Language,
272+
selectedValue = state.containerLanguageIndex,
273+
options = gameLanguageOptions,
274+
onOptionSelected = onContainerLanguageSelected,
275+
)
276+
261277
Spacer(Modifier.height(24.dp + navBarBottomPadding))
262278
}
263279
}

app/src/main/feature/stores/steam/enums/Language.kt

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,53 @@
11
package com.winlator.cmod.feature.stores.steam.enums
22
import timber.log.Timber
33

4-
enum class Language {
5-
english,
6-
german,
7-
french,
8-
italian,
9-
koreana,
10-
spanish,
11-
schinese,
12-
sc_schinese,
13-
tchinese,
14-
russian,
15-
japanese,
16-
polish,
17-
brazilian,
18-
latam,
19-
vietnamese,
20-
portuguese,
21-
danish,
22-
dutch,
23-
swedish,
24-
norwegian,
25-
finnish,
26-
turkish,
27-
thai,
28-
czech,
29-
unknown,
4+
enum class Language(val displayName: String) {
5+
english("English"),
6+
german("German"),
7+
french("French"),
8+
italian("Italian"),
9+
koreana("Korean"),
10+
spanish("Spanish"),
11+
schinese("Simplified Chinese"),
12+
sc_schinese("Simplified Chinese (SC)"),
13+
tchinese("Traditional Chinese"),
14+
russian("Russian"),
15+
japanese("Japanese"),
16+
polish("Polish"),
17+
brazilian("Portuguese (Brazil)"),
18+
latam("Spanish (Latin America)"),
19+
vietnamese("Vietnamese"),
20+
portuguese("Portuguese"),
21+
danish("Danish"),
22+
dutch("Dutch"),
23+
swedish("Swedish"),
24+
norwegian("Norwegian"),
25+
finnish("Finnish"),
26+
turkish("Turkish"),
27+
thai("Thai"),
28+
czech("Czech"),
29+
unknown("Unknown"),
3030
;
3131

3232
companion object {
33+
/** All display names in enum order, excluding [unknown] and the non-standard [sc_schinese] duplicate. */
34+
fun displayLabels(): List<String> =
35+
entries.filter { it != unknown && it != sc_schinese }.map { it.displayName }
36+
37+
/** Index of the entry whose [name] matches [containerLang], or the index of [english] as fallback. */
38+
fun indexForContainerLang(containerLang: String?): Int {
39+
val code = containerLang?.lowercase() ?: return 0
40+
val filtered = entries.filter { it != unknown && it != sc_schinese }
41+
val match = filtered.indexOfFirst { it.name == code }
42+
return if (match >= 0) match else 0
43+
}
44+
45+
/** Container language enum name for a given display-name [index] (0-based into the filtered list). */
46+
fun containerLangForIndex(index: Int): String {
47+
val filtered = entries.filter { it != unknown && it != sc_schinese }
48+
return filtered.getOrNull(index)?.name ?: english.name
49+
}
50+
3351
fun from(keyValue: String?): Language =
3452
when (keyValue?.lowercase()) {
3553
english.name -> {

0 commit comments

Comments
 (0)