Skip to content
Open
2 changes: 1 addition & 1 deletion app/src/main/app/shell/UnifiedActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10019,7 +10019,7 @@ class UnifiedActivity :
} catch (e: Exception) {
0L
}
val isInstallEnabled = totalInstallSize == 0L || availableBytes >= totalInstallSize
val isInstallEnabled = installed == true || totalInstallSize == 0L || availableBytes >= totalInstallSize
val installPathDisplay = customPath ?: SteamService.defaultAppInstallPath

val dlcItems =
Expand Down
Binary file modified app/src/main/assets/wnsteam/bionic/steam.exe
Binary file not shown.
160 changes: 150 additions & 10 deletions app/src/main/cpp/wn-steam-launcher/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,24 @@ static void stage_steam_config(void) {
}
}

// Escape a free-text value for a VDF/ACF quoted field: double backslashes, then
// escape quotes and newlines. Mirrors the Kotlin escapeString() so the C++ and
// Kotlin manifest paths produce identical, well-formed output.
static std::string vdf_escape(const char* s) {
std::string out;
if (!s) return out;
for (const char* p = s; *p; ++p) {
switch (*p) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
default: out += *p; break;
}
}
return out;
}

static void stage_app_manifest(uint32_t appId, const char* gameExe) {
if (appId == 0 || !gameExe) return;
const char* marker = "\\steamapps\\common\\";
Expand Down Expand Up @@ -258,33 +276,155 @@ static void stage_app_manifest(uint32_t appId, const char* gameExe) {
snprintf(acf, sizeof(acf),
"C:\\Program Files (x86)\\Steam\\steamapps\\appmanifest_%u.acf",
appId);
const char* owner = getenv("WN_STEAM_STEAMID");
const char* depotsEnv = getenv("WN_STEAM_DEPOTS");
const char* sharedEnv = getenv("WN_STEAM_SHARED_DEPOTS");
const char* appName = getenv("WN_STEAM_APP_NAME");
const char* installScriptsEnv = getenv("WN_STEAM_INSTALL_SCRIPTS");
const char* language = getenv("WN_STEAM_LANGUAGE");
const char* buildIdStr = getenv("WN_STEAM_BUILD_ID");
const char* sizeOnDiskStr = getenv("WN_STEAM_SIZE_ON_DISK");
const char* bytesToDownloadStr = getenv("WN_STEAM_BYTES_TO_DOWNLOAD");
const char* bytesToStageStr = getenv("WN_STEAM_BYTES_TO_STAGE");
if (!appName || !*appName) appName = installdir;
if (!language || !*language) language = "english";
unsigned long long buildId = (buildIdStr && *buildIdStr) ? strtoull(buildIdStr, NULL, 10) : 0ULL;
unsigned long long sizeOnDisk = (sizeOnDiskStr && *sizeOnDiskStr) ? strtoull(sizeOnDiskStr, NULL, 10) : 0ULL;
unsigned long long bytesToDownload = (bytesToDownloadStr && *bytesToDownloadStr) ? strtoull(bytesToDownloadStr, NULL, 10) : 0ULL;
unsigned long long bytesToStage = (bytesToStageStr && *bytesToStageStr) ? strtoull(bytesToStageStr, NULL, 10) : 0ULL;
FILE* f = fopen(acf, "w");
if (!f) {
log_line("[wn-launcher] app manifest: fopen(%s) failed", acf);
return;
}
const char* owner = getenv("WN_STEAM_STEAMID");
std::string nameEsc = vdf_escape(appName);
std::string installdirEsc = vdf_escape(installdir);
std::string languageEsc = vdf_escape(language);
fprintf(f,
"\"AppState\"\n"
"{\n"
"\t\"appid\"\t\t\"%u\"\n"
"\t\"Universe\"\t\t\"1\"\n"
"\t\"universe\"\t\t\"1\"\n"
"\t\"LauncherPath\"\t\t\"C:\\\\Program Files (x86)\\\\Steam\\\\steam.exe\"\n"
"\t\"name\"\t\t\"%s\"\n"
"\t\"StateFlags\"\t\t\"4\"\n"
"\t\"installdir\"\t\t\"%s\"\n"
"\t\"LastUpdated\"\t\t\"0\"\n"
"\t\"SizeOnDisk\"\t\t\"0\"\n"
"\t\"buildid\"\t\t\"0\"\n"
"\t\"LastUpdated\"\t\t\"%llu\"\n"
"\t\"LastPlayed\"\t\t\"0\"\n"
"\t\"SizeOnDisk\"\t\t\"%llu\"\n"
"\t\"StagingSize\"\t\t\"0\"\n"
"\t\"buildid\"\t\t\"%llu\"\n"
"\t\"LastOwner\"\t\t\"%s\"\n"
"\t\"InstalledDepots\"\n"
"\t\"DownloadType\"\t\t\"1\"\n"
"\t\"UpdateResult\"\t\t\"0\"\n"
"\t\"BytesToDownload\"\t\t\"%llu\"\n"
"\t\"BytesDownloaded\"\t\t\"%llu\"\n"
"\t\"BytesToStage\"\t\t\"%llu\"\n"
"\t\"BytesStaged\"\t\t\"%llu\"\n"
"\t\"TargetBuildID\"\t\t\"%llu\"\n"
"\t\"AutoUpdateBehavior\"\t\t\"0\"\n"
"\t\"AllowOtherDownloadsWhileRunning\"\t\t\"0\"\n"
"\t\"ScheduledAutoUpdate\"\t\t\"0\"\n",
appId, nameEsc.c_str(), installdirEsc.c_str(),
(unsigned long long)time(NULL),
sizeOnDisk, buildId,
(owner && *owner) ? owner : "0",
bytesToDownload, bytesToDownload,
bytesToStage, bytesToStage, buildId);
// Write InstalledDepots with depot data from WN_STEAM_DEPOTS env var.
// Format: depotId:manifestGid:size[:dlcAppId],...
if (depotsEnv && *depotsEnv) {
fprintf(f, "\t\"InstalledDepots\"\n\t{\n");
std::vector<char> buf(strlen(depotsEnv) + 1);
memcpy(buf.data(), depotsEnv, buf.size());
char* token = strtok(buf.data(), ",");
while (token) {
// Parse depotId:manifestGid:size[:dlcAppId]
char* colon1 = strchr(token, ':');
if (!colon1) { token = strtok(NULL, ","); continue; }
*colon1 = '\0';
const char* depotIdStr = token;
char* manifestStart = colon1 + 1;
char* colon2 = strchr(manifestStart, ':');
if (!colon2) { token = strtok(NULL, ","); continue; }
*colon2 = '\0';
const char* manifestStr = manifestStart;
char* sizeStart = colon2 + 1;
char* colon3 = strchr(sizeStart, ':');
const char* sizeStr, *dlcAppIdStr;
if (colon3) {
*colon3 = '\0';
sizeStr = sizeStart;
dlcAppIdStr = colon3 + 1;
} else {
sizeStr = sizeStart;
dlcAppIdStr = NULL;
}
fprintf(f, "\t\t\"%s\"\n\t\t{\n"
"\t\t\t\"manifest\"\t\t\"%s\"\n"
"\t\t\t\"size\"\t\t\"%s\"\n",
depotIdStr, manifestStr, sizeStr);
if (dlcAppIdStr && *dlcAppIdStr) {
fprintf(f, "\t\t\t\"dlcappid\"\t\t\"%s\"\n", dlcAppIdStr);
}
fprintf(f, "\t\t}\n");
token = strtok(NULL, ",");
}
fprintf(f, "\t}\n");
} else {
fprintf(f, "\t\"InstalledDepots\"\n\t{\n\t}\n");
}
// Write InstallScripts from WN_STEAM_INSTALL_SCRIPTS env var.
// Format: depotId:scriptFilename,...
if (installScriptsEnv && *installScriptsEnv) {
fprintf(f, "\t\"InstallScripts\"\n\t{\n");
std::vector<char> isbuf(strlen(installScriptsEnv) + 1);
memcpy(isbuf.data(), installScriptsEnv, isbuf.size());
char* istoken = strtok(isbuf.data(), ",");
while (istoken) {
char* iscolon = strchr(istoken, ':');
if (!iscolon) { istoken = strtok(NULL, ","); continue; }
*iscolon = '\0';
std::string scriptEsc = vdf_escape(iscolon + 1);
fprintf(f, "\t\t\"%s\"\t\t\"%s\"\n", istoken, scriptEsc.c_str());
istoken = strtok(NULL, ",");
}
fprintf(f, "\t}\n");
}
// Write SharedDepots from WN_STEAM_SHARED_DEPOTS env var.
// Format: sourceDepotId:targetAppId,...
if (sharedEnv && *sharedEnv) {
fprintf(f, "\t\"SharedDepots\"\n\t{\n");
std::vector<char> sbuf(strlen(sharedEnv) + 1);
memcpy(sbuf.data(), sharedEnv, sbuf.size());
char* stoken = strtok(sbuf.data(), ",");
while (stoken) {
char* scolon = strchr(stoken, ':');
if (!scolon) { stoken = strtok(NULL, ","); continue; }
*scolon = '\0';
fprintf(f, "\t\t\"%s\"\t\t\"%s\"\n", stoken, scolon + 1);
stoken = strtok(NULL, ",");
}
fprintf(f, "\t}\n");
}
fprintf(f,
"\t\"UserConfig\"\n"
"\t{\n"
"\t\t\"language\"\t\t\"%s\"\n"
"\t}\n"
"\t\"MountedConfig\"\n"
"\t{\n"
"\t\t\"language\"\t\t\"%s\"\n"
"\t}\n"
"}\n",
appId, installdir, installdir,
(owner && *owner) ? owner : "0");
languageEsc.c_str(), languageEsc.c_str());
fclose(f);
log_line("[wn-launcher] app manifest staged: %s (installdir=\"%s\", StateFlags=4)",
acf, installdir);
log_line("[wn-launcher] app manifest staged: %s (installdir=\"%s\", "
"depots=%s shared=%s scripts=%s)",
acf, installdir,
depotsEnv && *depotsEnv ? depotsEnv : "(none)",
sharedEnv && *sharedEnv ? sharedEnv : "(none)",
installScriptsEnv && *installScriptsEnv ? installScriptsEnv : "(none)");
}

// Counts running game processes (matches LaunchApp's canonical name or the literal
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/feature/settings/other/OtherSettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import com.winlator.cmod.app.shell.UnifiedActivity
import com.winlator.cmod.app.update.UpdateChecker
import com.winlator.cmod.feature.shortcuts.FrontendExporter
import com.winlator.cmod.feature.setup.SetupWizardActivity
import com.winlator.cmod.feature.stores.steam.enums.Language
import com.winlator.cmod.feature.stores.steam.utils.PrefManager
import com.winlator.cmod.runtime.audio.midi.MidiManager
import com.winlator.cmod.runtime.display.environment.ImageFsInstaller
import com.winlator.cmod.shared.ui.toast.WinToast
Expand Down Expand Up @@ -118,6 +120,11 @@ class OtherSettingsFragment : Fragment() {
// AppCompatDelegate recreates attached activities automatically.
}
},
onContainerLanguageSelected = { index ->
val langName = Language.containerLangForIndex(index)
PrefManager.containerLanguage = langName
refresh()
},
onSoundFontSelected = { index ->
// Selection is display-only; no persistence in legacy code.
uiState = uiState.copy(soundFontIndex = index)
Expand Down Expand Up @@ -217,11 +224,17 @@ class OtherSettingsFragment : Fragment() {
ctx,
)

// Game container language
val containerLanguageLabels = Language.displayLabels()
val containerLanguageIndex = Language.indexForContainerLang(PrefManager.containerLanguage)

uiState =
OtherSettingsState(
checkForUpdates = preferences.getBoolean("check_for_updates", false),
languageLabels = languageLabels,
languageIndex = languageIndex,
containerLanguageLabels = containerLanguageLabels,
containerLanguageIndex = containerLanguageIndex,
soundFontFiles = soundFontFiles,
soundFontIndex = soundFontIndex,
winlatorPath = winlatorPath,
Expand Down
16 changes: 15 additions & 1 deletion app/src/main/feature/settings/other/OtherSettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ data class OtherSettingsState(
val checkForUpdates: Boolean = true,
val languageLabels: List<String> = emptyList(),
val languageIndex: Int = 0,
val containerLanguageLabels: List<String> = emptyList(),
val containerLanguageIndex: Int = 0,
val soundFontFiles: List<String> = emptyList(),
val soundFontIndex: Int = 0,
val winlatorPath: String = "",
Expand Down Expand Up @@ -139,6 +141,7 @@ fun OtherSettingsScreen(
onCheckForUpdatesChanged: (Boolean) -> Unit,
onCheckForUpdatesNow: () -> Unit,
onLanguageSelected: (Int) -> Unit,
onContainerLanguageSelected: (Int) -> Unit,
onSoundFontSelected: (Int) -> Unit,
onInstallSoundFont: () -> Unit,
onRemoveSoundFont: () -> Unit,
Expand Down Expand Up @@ -210,8 +213,19 @@ fun OtherSettingsScreen(
onOptionSelected = onLanguageSelected,
)

SectionLabel(stringResource(R.string.settings_audio_sound), modifier = Modifier.padding(top = 8.dp))

SettingsDropdownCard(
title = stringResource(R.string.settings_other_game_language_title),
subtitle = stringResource(R.string.settings_other_game_language_summary),
icon = Icons.Outlined.Language,
options = state.containerLanguageLabels,
selectedIndex = state.containerLanguageIndex,
onOptionSelected = onContainerLanguageSelected,
)



SectionLabel(stringResource(R.string.settings_audio_sound), modifier = Modifier.padding(top = 8.dp))
SoundFontCard(
files = state.soundFontFiles,
selectedIndex = state.soundFontIndex,
Expand Down
70 changes: 44 additions & 26 deletions app/src/main/feature/stores/steam/enums/Language.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
package com.winlator.cmod.feature.stores.steam.enums
import timber.log.Timber

enum class Language {
english,
german,
french,
italian,
koreana,
spanish,
schinese,
sc_schinese,
tchinese,
russian,
japanese,
polish,
brazilian,
latam,
vietnamese,
portuguese,
danish,
dutch,
swedish,
norwegian,
finnish,
turkish,
thai,
czech,
unknown,
enum class Language(val displayName: String) {
english("English"),
german("German"),
french("French"),
italian("Italian"),
koreana("Korean"),
spanish("Spanish"),
schinese("Simplified Chinese"),
sc_schinese("Simplified Chinese (SC)"),
tchinese("Traditional Chinese"),
russian("Russian"),
japanese("Japanese"),
polish("Polish"),
brazilian("Portuguese (Brazil)"),
latam("Spanish (Latin America)"),
vietnamese("Vietnamese"),
portuguese("Portuguese"),
danish("Danish"),
dutch("Dutch"),
swedish("Swedish"),
norwegian("Norwegian"),
finnish("Finnish"),
turkish("Turkish"),
thai("Thai"),
czech("Czech"),
unknown("Unknown"),
;

companion object {
/** All display names in enum order, excluding [unknown] and the non-standard [sc_schinese] duplicate. */
fun displayLabels(): List<String> =
entries.filter { it != unknown && it != sc_schinese }.map { it.displayName }

/** Index of the entry whose [name] matches [containerLang], or the index of [english] as fallback. */
fun indexForContainerLang(containerLang: String?): Int {
val code = containerLang?.lowercase() ?: return 0
val filtered = entries.filter { it != unknown && it != sc_schinese }
val match = filtered.indexOfFirst { it.name == code }
return if (match >= 0) match else 0
}

/** Container language enum name for a given display-name [index] (0-based into the filtered list). */
fun containerLangForIndex(index: Int): String {
val filtered = entries.filter { it != unknown && it != sc_schinese }
return filtered.getOrNull(index)?.name ?: english.name
}

fun from(keyValue: String?): Language =
when (keyValue?.lowercase()) {
english.name -> {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/feature/stores/steam/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,7 @@ class SteamService : Service() {
for (dlcApp in indirectDlcApps) {
val entitledDlcDepotIds = getEntitledDepotIds(dlcApp.packageId)
for ((depotId, depot) in dlcApp.depots) {

if (isDepotEntitled(depotId, depot, entitledDlcDepotIds) &&
filterForDownloadableDepots(depot, has64Bit, preferredLanguage, null)
) {
Expand All @@ -1728,7 +1729,7 @@ class SteamService : Service() {
language = depot.language,
lowViolence = depot.lowViolence,
manifests = depot.manifests,
encryptedManifests = depot.encryptedManifests,
encryptedManifests = depot.encryptedManifests
)
}
}
Expand Down Expand Up @@ -4173,6 +4174,7 @@ class SteamService : Service() {
.distinct(),
)
}
mainAppDlcIds.addAll(calculatedDlcAppIds)

runBlocking(Dispatchers.IO) {
if (mainAppDepots.isNotEmpty()) {
Expand Down
Loading