Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 91 additions & 1 deletion gameScanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,39 @@
except ImportError:
pass # Just needed for pycharm comments

def _decodeVDFPath(vdfPath):
# type: (str) -> str
"""
Decode the small amount of escaping Steam uses for paths in VDF files.
"""
return vdfPath.replace("\\\\", "\\")

def _winePathToHostPath(winePath, bottlePath):
# type: (str, str) -> Optional[str]
"""
Convert a Windows path inside a Wine/CrossOver bottle to a host path.

For example:
C:\\Program Files (x86)\\Steam -> <bottlePath>/drive_c/Program Files (x86)/Steam
"""
winePath = _decodeVDFPath(winePath)
match = re.match(r'^([a-zA-Z]):[\\/]*(.*)$', winePath)
if not match:
return os.path.realpath(os.path.expanduser(winePath))

driveLetter = match.group(1).lower()
relativePath = match.group(2).replace("\\", "/")

dosDevicePath = os.path.join(bottlePath, "dosdevices", "{}:".format(driveLetter))
if os.path.exists(dosDevicePath):
hostDrivePath = os.path.realpath(dosDevicePath)
elif driveLetter == 'c':
hostDrivePath = os.path.join(bottlePath, "drive_c")
else:
return None

return os.path.realpath(os.path.join(hostDrivePath, relativePath))

def findAdditionalSteamLibraries(mainSteamPath):
# type: (str) -> List[str]
r"""
Expand All @@ -39,6 +72,59 @@ def findAdditionalSteamLibraries(mainSteamPath):
traceback.print_exc()
return []

def findAdditionalSteamLibrariesInCrossOverBottle(mainSteamPath, bottlePath):
# type: (str, str) -> List[str]
r"""
Try to locate additional Steam libraries for a Windows Steam install inside
a CrossOver bottle, returning native macOS paths.
"""
try:
vdf = os.path.join(mainSteamPath, "steamapps", "libraryfolders.vdf")
if not os.path.exists(vdf):
return []

import io
baseInstallFolderRegex = re.compile(r'^\s*"path"\s+"([^"]+)"', re.MULTILINE)
with io.open(vdf, 'r', encoding='UTF-8') as vdfFile:
libraries = []
for steamLibraryPath in baseInstallFolderRegex.findall(vdfFile.read()):
hostPath = _winePathToHostPath(steamLibraryPath, bottlePath)
if hostPath and os.path.isdir(os.path.join(hostPath, "steamapps")):
libraries.append(hostPath)
return libraries
except:
traceback.print_exc()
return []

def findPossibleCrossOverSteamPathsMac(crossoverBottlesPath=None):
# type: (Optional[str]) -> List[str]
r"""
Find Windows Steam installs inside CrossOver bottles on macOS.
Returns Steam root paths, not the steamapps/common folders.
"""
if crossoverBottlesPath is None:
crossoverBottlesPath = os.path.expanduser("~/Library/Application Support/CrossOver/Bottles")

if not os.path.isdir(crossoverBottlesPath):
return []

allSteamPaths = []
for bottlePath in glob.glob(os.path.join(crossoverBottlesPath, "*")):
if not os.path.isdir(bottlePath):
continue

candidateSteamPaths = [
os.path.join(bottlePath, "drive_c", "Program Files (x86)", "Steam"),
os.path.join(bottlePath, "drive_c", "Program Files", "Steam")
]

for steamPath in candidateSteamPaths:
if os.path.isdir(os.path.join(steamPath, "steamapps")):
allSteamPaths.append(os.path.realpath(steamPath))
allSteamPaths += findAdditionalSteamLibrariesInCrossOverBottle(steamPath, bottlePath)

return deDuplicatePaths(allSteamPaths)

def getSecondarySteamPaths(baseSteamPaths):
# type: (List[str]) -> List[str]
r"""
Expand Down Expand Up @@ -170,6 +256,10 @@ def getMaybeGamePaths():
if common.Globals.IS_MAC:
hardCodedGameContainingPaths.append("~/Library/Application Support/Steam/steamapps/common")
hardCodedGameContainingPaths.append("~/GOG Games") # Not sure if this is correct for MacOS
hardCodedGameContainingPaths += [
os.path.join(steamPath, "steamapps", "common")
for steamPath in findPossibleCrossOverSteamPathsMac()
]
if common.Globals.IS_WINDOWS:
hardCodedGameContainingPaths.append("c:/games/Mangagamer")
hardCodedGameContainingPaths.append("c:/GOG Games")
Expand All @@ -184,7 +274,7 @@ def getMaybeGamePaths():

# Try to find secondary steam folders. Need to remove the 'steamapps/common' part of path to get base steam path
try:
baseHardCodedSteamPaths = [os.path.split(os.path.split(p)[0])[0] for p in hardCodedGameContainingPaths if 'steam' in p.lower()]
baseHardCodedSteamPaths = [os.path.split(os.path.split(p)[0])[0] for p in hardCodedGameContainingPaths if 'steam' in p.lower() and 'crossover/bottles' not in p.lower()]
hardCodedGameContainingPaths += [os.path.realpath(os.path.join(p, "steamapps", "common")) for p in getSecondarySteamPaths(baseHardCodedSteamPaths)]
except:
traceback.print_exc()
Expand Down
8 changes: 5 additions & 3 deletions higurashiInstaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ def _applyLanguageSpecificSharedAssets(self, folderToApply):
osString = common.Globals.OS_STRING
if self.isWine:
osString = "windows"
print("Language Patch UI: Proton/Wine detected! Forcing install of Windows sharedassets0.assets.")
print("Language Patch UI: Proton/Wine/CrossOver detected! Forcing install of Windows sharedassets0.assets.")

# TODO: use the sharedassets0.assets.backup to determine store name?
# For now, only differentiate steam/non-steam
Expand Down Expand Up @@ -498,8 +498,10 @@ def saveFileVersionInfoFinished(self, forcedSaveFolder=None):
def main(fullInstallConfiguration):
# type: (installConfiguration.FullInstallConfiguration) -> None

if common.Globals.IS_LINUX:
print("Linux Compatibility Layer: {}".format("YES: Using Wine or Proton" if fullInstallConfiguration.isWine else "no: Using Native"))
if fullInstallConfiguration.isWine:
print("Compatibility Layer: YES: Using Wine, Proton, or CrossOver")
elif common.Globals.IS_LINUX:
print("Linux Compatibility Layer: no: Using Native")

isVoiceOnly = fullInstallConfiguration.subModConfig.subModName == 'voice-only'
if isVoiceOnly:
Expand Down
2 changes: 1 addition & 1 deletion installConfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __init__(self, subModConfig, path, isSteam):
self.useIPV6 = False
self.unityVersion = None

if self.subModConfig.family == "higurashi" and common.Globals.IS_LINUX and higurashiWindowsExecutableExists(self.subModConfig.dataName, self.installPath):
if self.subModConfig.family == "higurashi" and (not common.Globals.IS_WINDOWS) and higurashiWindowsExecutableExists(self.subModConfig.dataName, self.installPath):
self.isWine = True
else:
self.isWine = False
Expand Down
92 changes: 92 additions & 0 deletions installerTests/testGameScanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import os
import shutil
import tempfile
import unittest

import common
import gameScanner
import installConfiguration


class DummySubModConfig:
autodetect = True
family = "higurashi"
dataName = "HigurashiEp08_Data"
identifiers = ["HigurashiEp08_Data"]
modName = "Matsuribayashi Ch.8"


class TestCrossOverGameScanner(unittest.TestCase):
def setUp(self):
self.tempDir = tempfile.mkdtemp()

def tearDown(self):
shutil.rmtree(self.tempDir)

def test_finds_windows_steam_in_crossover_bottle(self):
bottlesPath = os.path.join(self.tempDir, "Bottles")
bottlePath = os.path.join(bottlesPath, "Steam")
steamPath = os.path.join(bottlePath, "drive_c", "Program Files (x86)", "Steam")
secondarySteamPath = os.path.join(bottlePath, "drive_c", "SteamLibrary")
commonPath = os.path.join(steamPath, "steamapps", "common")
secondaryCommonPath = os.path.join(secondarySteamPath, "steamapps", "common")
os.makedirs(commonPath)
os.makedirs(secondaryCommonPath)

libraryFoldersPath = os.path.join(steamPath, "steamapps", "libraryfolders.vdf")
with open(libraryFoldersPath, "w") as libraryFoldersFile:
libraryFoldersFile.write(
'"libraryfolders"\n'
'{\n'
'\t"0"\n'
'\t{\n'
'\t\t"path"\t\t"C:\\\\Program Files (x86)\\\\Steam"\n'
'\t}\n'
'\t"1"\n'
'\t{\n'
'\t\t"path"\t\t"C:\\\\SteamLibrary"\n'
'\t}\n'
'}\n'
)

self.assertEqual(
gameScanner.findPossibleCrossOverSteamPathsMac(bottlesPath),
[os.path.realpath(steamPath), os.path.realpath(secondarySteamPath)]
)

def test_scan_marks_crossover_higurashi_install_as_wine(self):
gamePath = os.path.join(self.tempDir, "Higurashi When They Cry Hou - Ch.8 Matsuribayashi")
os.makedirs(os.path.join(gamePath, "HigurashiEp08_Data"))
open(os.path.join(gamePath, "HigurashiEp08.exe"), "w").close()
open(os.path.join(gamePath, "steam_api.dll"), "w").close()

originalIsWindows = common.Globals.IS_WINDOWS
try:
common.Globals.IS_WINDOWS = False
fullConfigs, partiallyUninstalledPaths = gameScanner.scanForFullInstallConfigs(
[DummySubModConfig()],
possiblePaths=[gamePath]
)
self.assertEqual(partiallyUninstalledPaths, [])
self.assertEqual(len(fullConfigs), 1)
self.assertTrue(fullConfigs[0].isWine)
self.assertTrue(fullConfigs[0].isSteam)
finally:
common.Globals.IS_WINDOWS = originalIsWindows

def test_higurashi_windows_exe_is_wine_on_non_windows(self):
gamePath = os.path.join(self.tempDir, "Higurashi When They Cry Hou - Ch.8 Matsuribayashi")
os.makedirs(gamePath)
open(os.path.join(gamePath, "HigurashiEp08.exe"), "w").close()

originalIsWindows = common.Globals.IS_WINDOWS
try:
common.Globals.IS_WINDOWS = False
fullConfig = installConfiguration.FullInstallConfiguration(DummySubModConfig(), gamePath, True)
self.assertTrue(fullConfig.isWine)

common.Globals.IS_WINDOWS = True
fullConfig = installConfiguration.FullInstallConfiguration(DummySubModConfig(), gamePath, True)
self.assertFalse(fullConfig.isWine)
finally:
common.Globals.IS_WINDOWS = originalIsWindows