#NoTrayIcon #Region ;**** Directives created by AutoIt3Wrapper_GUI **** #AutoIt3Wrapper_Icon=enderal.ico #AutoIt3Wrapper_Res_File_Add=background.jpg, RT_RCDATA, BACKGROUND #EndRegion ;**** Directives created by AutoIt3Wrapper_GUI **** #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ; ============================================ ; Constants and Global Variables ; ============================================ Global Const $LAUNCHER_WIDTH = 945 Global Const $LAUNCHER_HEIGHT = 578 Global Const $SETTINGS_MARGIN = 50 Global Const $SETTINGS_WIDTH = $LAUNCHER_WIDTH - ($SETTINGS_MARGIN * 2) Global Const $SETTINGS_HEIGHT = $LAUNCHER_HEIGHT - ($SETTINGS_MARGIN * 2) Global Const $TAB_LEFT_MARGIN = 42 Global Const $TAB_TOP_MARGIN = 74 Global Const $TAB_ROW_HEIGHT = 42 Global Const $LOCALAPPDATA = EnvGet("LOCALAPPDATA") Global Const $REG_KEY = "HKEY_CURRENT_USER\Software\SureAI\EnderalSE" Global Const $REG_INSTALL_PATH = "Install_Path" Global Const $REG_EXE_PATH = "Exe_Path" Global Const $REG_START_STEAM = "Start_Steam" Global Const $AA_TAA = "TAA (Best quality)" Global Const $AA_FXAA = "FXAA (Low)" Global Const $AA_OFF = "Off (Best performance)" ; Plugin load order classes - the engine always loads masters before regular plugins Global Const $PLUGIN_CLASS_MASTER = 1 Global Const $PLUGIN_CLASS_NORMAL = 2 Global Const $PLUGIN_FLAG_MASTER = 0x00000001 Global Const $PLUGIN_FLAG_LIGHT = 0x00000200 ; Check box images of the plugin list (state image indexes) Global Const $PLUGIN_STATE_UNCHECKED = 1 Global Const $PLUGIN_STATE_CHECKED = 2 Global Const $PLUGIN_STATE_UNCHECKED_DIMMED = 3 Global Const $PLUGIN_STATE_CHECKED_DIMMED = 4 ; Theme parts used to draw them (BUTTON class) Global Const $THEME_BP_CHECKBOX = 3 Global Const $THEME_CB_UNCHECKED = 1 Global Const $THEME_CB_UNCHECKED_DISABLED = 4 Global Const $THEME_CB_CHECKED = 5 Global Const $THEME_CB_CHECKED_DISABLED = 8 Global $PLUGINS_DIR = "" Global $PLUGINS_FILE = "" Global $DOCUMENTS_DIR = "" Global $ENDERAL_INI = "" Global $ENDERAL_PREFS_INI = "" Global $LAUNCHER_DIR = @ScriptDir ; GUI handles Global $hMainGUI, $hSettingsGUI Global $btnPlay, $btnSettings, $btnExit Global $btnClose, $btnMinimize Global $lblCredits, $lblWiki, $lblSupport, $lblChangelog Global $lblMinimizeShadow, $lblCloseShadow Global $lblCreditsShadow, $lblWikiShadow, $lblSupportShadow, $lblChangelogShadow Global $hBackgroundPic = 0 ; Settings controls Global $tabSettings Global $btnSave, $btnCancel ; Display tab controls Global $lblResolution, $cmbResolution Global $lblAntialiasing, $cmbAntialiasing Global $chkWindowed Global $btnLow, $btnMedium, $btnHigh, $btnUltra ; Rendering tab controls Global $chk64bitRT, $chkSSAO, $chkVolumetricLighting, $chkSSR ; SSE Display Tweaks controls Global $grpSSEDisplayTweaks, $chkSSEDisplayTweaks Global $lblFramerateLimit, $sldFramerateLimit, $lblFramerateLimitValue ; Controls tab controls Global $chkController, $chkVibration Global $chkStartSteam Global $grpIniFiles, $btnOpenEnderalIni, $btnOpenEnderalPrefsIni, $btnOpenCustomIni, $btnOpenModIni, $btnOpenSavegames ; Plugins tab controls Global $lvPlugins, $hLvPlugins = 0 Global $btnPluginUp, $btnPluginDown, $btnPluginsEnableAll, $btnPluginsDisableAll Global $lblPluginsCount, $lblPluginsHint Global $hPluginStateImages = 0 ; Plugin list state ; [i][0] = file name, [i][1] = enabled, [i][2] = locked (always loaded by the game), ; [i][3] = load order class, [i][4] = type label, [i][5] = present in plugins.txt Global $aPluginList[1][6] Global $iPluginListCount = 0 Global $bPluginListLoaded = False Global $bPluginListDirty = False Global $bPluginListUpdating = False Global $bPluginListReadOnly = False ; Steam version detection Global $bIsSteamVersion = False ; Mod Organizer 2 directory, empty when the launcher was not started by MO2 Global $MO2_DIR = "" ; ============================================ ; Helper Functions ; ============================================ Func DirExists($path) Return FileExists($path) And StringInStr(FileGetAttrib($path), "D") EndFunc Func SetCheckboxState($ctrl, $value, $checkedValue = "1") If $value = $checkedValue Then GUICtrlSetState($ctrl, $GUI_CHECKED) Else GUICtrlSetState($ctrl, $GUI_UNCHECKED) EndIf EndFunc Func GetIniFileName($filePath) Return StringRegExpReplace($filePath, ".*\\", "") EndFunc Func GetCustomIniPath() ; Returns the path to EnderalCustom.ini or SkyrimCustom.ini based on current INI mode Local $iniFileName = GetIniFileName($ENDERAL_INI) If StringInStr($iniFileName, "Enderal") Then Return $DOCUMENTS_DIR & "\EnderalCustom.ini" Else Return $DOCUMENTS_DIR & "\SkyrimCustom.ini" EndIf EndFunc Func GetCustomIniFileName() ; Returns just the filename (EnderalCustom.ini or SkyrimCustom.ini) Local $iniFileName = GetIniFileName($ENDERAL_INI) If StringInStr($iniFileName, "Enderal") Then Return "EnderalCustom.ini" Else Return "SkyrimCustom.ini" EndIf EndFunc Func GetModIniFileName() ; Returns just the filename for the Mod INI Return "Enderal - Forgotten Stories.ini" EndFunc Func GetDataIniPath() ; Returns the path to Data\Enderal - Forgotten Stories.ini Return $LAUNCHER_DIR & "\Data\" & GetModIniFileName() EndFunc Func IniReadWithCustom($section, $key, $default) ; Read with precedence: Data INI > Custom INI > Base INI ; 1. Check Data\Enderal - Forgotten Stories.ini first (highest priority) Local $dataIniPath = GetDataIniPath() If FileExists($dataIniPath) Then Local $dataValue = IniRead($dataIniPath, $section, $key, "") If $dataValue <> "" Then Return $dataValue EndIf EndIf ; 2. Check custom INI (EnderalCustom.ini or SkyrimCustom.ini) Local $customIniPath = GetCustomIniPath() If FileExists($customIniPath) Then Local $customValue = IniRead($customIniPath, $section, $key, "") If $customValue <> "" Then Return $customValue EndIf EndIf ; 3. Fall back to base INI Return IniRead($ENDERAL_INI, $section, $key, $default) EndFunc Func IniWriteWithCustom($section, $key, $value) ; Write with precedence: Data INI > Custom INI > Base INI ; 1. Check Data\Enderal - Forgotten Stories.ini first (highest priority) Local $dataIniPath = GetDataIniPath() If FileExists($dataIniPath) Then Local $dataValue = IniRead($dataIniPath, $section, $key, "") If $dataValue <> "" Then Return IniWrite($dataIniPath, $section, $key, $value) EndIf EndIf ; 2. Check custom INI (EnderalCustom.ini or SkyrimCustom.ini) Local $customIniPath = GetCustomIniPath() If FileExists($customIniPath) Then Local $customValue = IniRead($customIniPath, $section, $key, "") If $customValue <> "" Then Return IniWrite($customIniPath, $section, $key, $value) EndIf EndIf ; 3. Fall back to base INI Return IniWrite($ENDERAL_INI, $section, $key, $value) EndFunc Func UpdateCustomIniButtonState() ; Enable or disable the custom INI button based on file existence Local $customIniPath = GetCustomIniPath() Local $customIniName = GetCustomIniFileName() If FileExists($customIniPath) Then GUICtrlSetData($btnOpenCustomIni, $customIniName) GUICtrlSetState($btnOpenCustomIni, $GUI_ENABLE) Else GUICtrlSetData($btnOpenCustomIni, $customIniName & " (N/A)") GUICtrlSetState($btnOpenCustomIni, $GUI_DISABLE) EndIf EndFunc Func UpdateModIniButtonState() ; Enable or disable the Mod INI button based on file existence Local $modIniPath = GetDataIniPath() Local $modIniName = GetModIniFileName() If FileExists($modIniPath) Then GUICtrlSetData($btnOpenModIni, $modIniName) GUICtrlSetState($btnOpenModIni, $GUI_ENABLE) Else GUICtrlSetData($btnOpenModIni, $modIniName & " (N/A)") GUICtrlSetState($btnOpenModIni, $GUI_DISABLE) EndIf EndFunc Func RunWithAssociation($filePath) ; Get file extension Local $ext = StringRegExpReplace($filePath, ".*(\.[^.]+)$", "$1") If $ext = $filePath Then Return False ; Get file type from registry Local $fileType = RegRead("HKEY_CLASSES_ROOT\" & $ext, "") If @error Or $fileType = "" Then ; Fallback to notepad if no association found Run('notepad.exe "' & $filePath & '"') Return True EndIf ; Get command for opening this file type Local $command = RegRead("HKEY_CLASSES_ROOT\" & $fileType & "\shell\open\command", "") If @error Or $command = "" Then ; Fallback to notepad if no command found Run('notepad.exe "' & $filePath & '"') Return True EndIf ; Replace %1 or %L with the file path, handle quoted and unquoted variants If StringInStr($command, "%1") Or StringInStr($command, "%L") Then $command = StringReplace($command, '"%1"', '"' & $filePath & '"') $command = StringReplace($command, "%1", '"' & $filePath & '"') $command = StringReplace($command, '"%L"', '"' & $filePath & '"') $command = StringReplace($command, "%L", '"' & $filePath & '"') Else ; Append file path if no placeholder $command = $command & ' "' & $filePath & '"' EndIf Run($command) Return True EndFunc Func WriteCheckboxToIni($ctrl, $file, $section, $key, $checkedVal = "1", $uncheckedVal = "0") If BitAND(GUICtrlRead($ctrl), $GUI_CHECKED) Then IniWrite($file, $section, $key, $checkedVal) Else IniWrite($file, $section, $key, $uncheckedVal) EndIf EndFunc Func WriteCheckboxWithCustom($ctrl, $section, $key, $checkedVal = "1", $uncheckedVal = "0") ; Write to custom INI if key exists there, otherwise to base INI ($ENDERAL_INI) If BitAND(GUICtrlRead($ctrl), $GUI_CHECKED) Then IniWriteWithCustom($section, $key, $checkedVal) Else IniWriteWithCustom($section, $key, $uncheckedVal) EndIf EndFunc ; ============================================ ; DPI Awareness Functions ; ============================================ Func SetDPIAwareness() ; Set DPI awareness for SkyrimSE.exe to prevent Windows scaling issues Local $exePath = $LAUNCHER_DIR & "\SkyrimSE.exe" If Not FileExists($exePath) Then Return Local $regKey = "HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" RegWrite($regKey, $exePath, "REG_SZ", "~ HIGHDPIAWARE") EndFunc ; ============================================ ; Version Detection Functions ; ============================================ Func GetSkyrimVersion() Local $exePath = $LAUNCHER_DIR & "\SkyrimSE.exe" If Not FileExists($exePath) Then Return "" ; Try FileVersion and ProductVersion first (older versions use these) Local $version = FileGetVersion($exePath, "FileVersion") If @error Or $version = "" Or $version = "1.0.0.0" Then $version = FileGetVersion($exePath, "ProductVersion") EndIf ; Fall back to standard version If @error Or $version = "" Or $version = "1.0.0.0" Then $version = FileGetVersion($exePath) EndIf If @error Or $version = "" Or $version = "1.0.0.0" Then Return "" ; Return only major.minor.build (strip revision) Local $parts = StringSplit($version, ".") If $parts[0] >= 3 Then Return $parts[1] & "." & $parts[2] & "." & $parts[3] EndIf Return $version EndFunc Func GetBaseDirectoryName() ; Returns "Skyrim Special Edition GOG" if Galaxy64.dll exists, otherwise "Skyrim Special Edition" If FileExists($LAUNCHER_DIR & "\Galaxy64.dll") Then Return "Skyrim Special Edition GOG" Else Return "Skyrim Special Edition" EndIf EndFunc Func GetEnderalBaseDirectoryName() ; Returns "Enderal Special Edition GOG" if Galaxy64.dll exists, otherwise "Enderal Special Edition" If FileExists($LAUNCHER_DIR & "\Galaxy64.dll") Then Return "Enderal Special Edition GOG" Else Return "Enderal Special Edition" EndIf EndFunc Func GetEnderalVersion() ; First try to read from EnderalVersion.ini (no section header, just key=value) Local $versionIni = $LAUNCHER_DIR & "\Data\SKSE\Plugins\EnderalVersion.ini" If FileExists($versionIni) Then Local $content = FileRead($versionIni) ; Match line starting with "version" followed by "=" and capture until end of line Local $match = StringRegExp($content, "(?im)^version\s*=\s*(.+)$", 1) If Not @error And UBound($match) > 0 Then Return StringStripWS($match[0], 3) EndIf EndIf ; Check if Enderal ESM exists (outdated installation) Local $esmPath = $LAUNCHER_DIR & "\Data\Enderal - Forgotten Stories.esm" If FileExists($esmPath) Then Return "outdated" ; Neither file exists Return "" EndFunc ; ============================================ ; Update Function ; ============================================ Func CheckAndApplyUpdate() ; Check if update is needed: SkyrimSE.exe is not 1.6.659 AND skse64_1_6_659.dll exists Local $skyrimVersion = GetSkyrimVersion() Local $oldSkseDll = $LAUNCHER_DIR & "\skse64_1_6_659.dll" If $skyrimVersion <> "1.6.659" And FileExists($oldSkseDll) Then ; Files to delete unconditionally Local $filesToDelete[] = [ _ "skse64_1_6_659.dll", _ "tbb.dll", _ "tbbmalloc.dll", _ "enderal readme.txt", _ "Skyrim_Default.ini", _ "Data\Skyrim - Patch.bsa", _ "Data\Enderal SE v2.0.12.4 Changelog.txt", _ "Data\SKSE\Plugins\JContainers64.dll", _ "Data\SKSE\Plugins\YesImSure.json", _ "Data\meshes\actors\character\character assets\skeleton.xml", _ "Data\meshes\actors\character\character assets female\skeleton_female.xml", _ "Data\SkyUI_SE.ini" _ ] ; Files to delete only if size matches [path, size] Local $filesToDeleteWithSize[][2] = [ _ ["Data\Interface\controls\pc\controlmap.txt", 8492], _ ["Data\Interface\exported\widgets\skyui\followerpanel.swf", 15541], _ ["Data\Interface\dialoguemenu.swf", 24622], _ ["Data\Interface\fonts_en2.swf", 33021], _ ["Data\Interface\hudmenu.swf", 372484], _ ["Data\Interface\startmenu.swf", 66683], _ ["Data\Interface\fontconfig.txt", 1211], _ ["Data\Scripts\ActiveMagicEffect.pex", 7497], _ ["Data\Scripts\Actor.pex", 14869], _ ["Data\Scripts\ActorBase.pex", 2643], _ ["Data\Scripts\ActorValueInfo.pex", 1791], _ ["Data\Scripts\Alias.pex", 3949], _ ["Data\Scripts\Ammo.pex", 549], _ ["Data\Scripts\Apparatus.pex", 522], _ ["Data\Scripts\Armor.pex", 4983], _ ["Data\Scripts\ArmorAddon.pex", 1169], _ ["Data\Scripts\Art.pex", 500], _ ["Data\Scripts\Book.pex", 574], _ ["Data\Scripts\Camera.pex", 1171], _ ["Data\Scripts\Cell.pex", 1264], _ ["Data\Scripts\ColorComponent.pex", 1206], _ ["Data\Scripts\ColorForm.pex", 1124], _ ["Data\Scripts\CombatStyle.pex", 3260], _ ["Data\Scripts\ConstructibleObject.pex", 1047], _ ["Data\Scripts\DefaultObjectManager.pex", 540], _ ["Data\Scripts\Enchantment.pex", 1149], _ ["Data\Scripts\EquipSlot.pex", 515], _ ["Data\Scripts\Faction.pex", 3819], _ ["Data\Scripts\Flora.pex", 651], _ ["Data\Scripts\Form.pex", 5057], _ ["Data\Scripts\FormList.pex", 813], _ ["Data\Scripts\FormType.pex", 7963], _ ["Data\Scripts\GameData.pex", 1898], _ ["Data\Scripts\HeadPart.pex", 1274], _ ["Data\Scripts\Ingredient.pex", 1369], _ ["Data\Scripts\Input.pex", 968], _ ["Data\Scripts\JArray.pex", 3575], _ ["Data\Scripts\JAtomic.pex", 1973], _ ["Data\Scripts\JContainers.pex", 1184], _ ["Data\Scripts\JContainers_DomainExample.pex", 14101], _ ["Data\Scripts\JDB.pex", 1373], _ ["Data\Scripts\JFormDB.pex", 1933], _ ["Data\Scripts\JFormMap.pex", 1675], _ ["Data\Scripts\JIntMap.pex", 1672], _ ["Data\Scripts\JLua.pex", 1162], _ ["Data\Scripts\JMap.pex", 1669], _ ["Data\Scripts\JString.pex", 944], _ ["Data\Scripts\JValue.pex", 2589], _ ["Data\Scripts\Keyword.pex", 714], _ ["Data\Scripts\LeveledActor.pex", 805], _ ["Data\Scripts\LeveledItem.pex", 1022], _ ["Data\Scripts\LeveledSpell.pex", 802], _ ["Data\Scripts\Location.pex", 1744], _ ["Data\Scripts\MagicEffect.pex", 2501], _ ["Data\Scripts\Math.pex", 1436], _ ["Data\Scripts\ModEvent.pex", 952], _ ["Data\Scripts\NetImmerse.pex", 2929], _ ["Data\Scripts\ObjectReference.pex", 16307], _ ["Data\Scripts\Outfit.pex", 505], _ ["Data\Scripts\Perk.pex", 1431], _ ["Data\Scripts\Potion.pex", 1298], _ ["Data\Scripts\Quest.pex", 5314], _ ["Data\Scripts\Race.pex", 6618], _ ["Data\Scripts\Scroll.pex", 1387], _ ["Data\Scripts\Shout.pex", 766], _ ["Data\Scripts\SKSE.pex", 845], _ ["Data\Scripts\SoulGem.pex", 506], _ ["Data\Scripts\Sound.pex", 723], _ ["Data\Scripts\SoundDescriptor.pex", 900], _ ["Data\Scripts\SpawnerTask.pex", 907], _ ["Data\Scripts\Spell.pex", 1673], _ ["Data\Scripts\StringUtil.pex", 1100], _ ["Data\Scripts\TextureSet.pex", 593], _ ["Data\Scripts\TreeObject.pex", 650], _ ["Data\Scripts\UI.pex", 2070], _ ["Data\Scripts\UICallback.pex", 1139], _ ["Data\Scripts\Utility.pex", 2658], _ ["Data\Scripts\Weapon.pex", 3259], _ ["Data\Scripts\Weather.pex", 1107], _ ["Data\Scripts\WornObject.pex", 1629] _ ] ; Delete unconditional files For $i = 0 To UBound($filesToDelete) - 1 Local $fullPath = $LAUNCHER_DIR & "\" & $filesToDelete[$i] If FileExists($fullPath) Then FileDelete($fullPath) EndIf Next ; Delete files only if size matches For $i = 0 To UBound($filesToDeleteWithSize) - 1 Local $fullPath = $LAUNCHER_DIR & "\" & $filesToDeleteWithSize[$i][0] If FileExists($fullPath) Then Local $fileSize = FileGetSize($fullPath) If $fileSize = $filesToDeleteWithSize[$i][1] Then FileDelete($fullPath) EndIf EndIf Next ; Delete directories Local $dirsToDelete[] = [ _ "Data\SKSE\Plugins\JCData", _ "Skyrim" _ ] For $i = 0 To UBound($dirsToDelete) - 1 Local $fullPath = $LAUNCHER_DIR & "\" & $dirsToDelete[$i] If FileExists($fullPath) Then DirRemove($fullPath, 1) EndIf Next ; Set borderless windowed mode in EnderalPrefs.ini if it exists Local $prefsPath = GetDocumentsPath() & "\My Games\" & GetEnderalBaseDirectoryName() & "\EnderalPrefs.ini" If FileExists($prefsPath) Then IniWrite($prefsPath, "Display", "bBorderless", "1") IniWrite($prefsPath, "Display", "bFull Screen", "0") EndIf MsgBox(64, "Update", "Enderal SE has been successfully updated.") EndIf EndFunc ; ============================================ ; Main Entry Point ; ============================================ Main() Func Main() ; Check and apply update if needed CheckAndApplyUpdate() ; Check if SkyrimRedirector is installed Local $redirectorDll = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SkyrimRedirector.dll" Local $redirectorIni = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SkyrimRedirector.ini" If FileExists($redirectorDll) Then If FileExists($redirectorIni) Then ; Read paths from SkyrimRedirector.ini $ENDERAL_INI = IniRead($redirectorIni, "Redirection", "Ini", "") $ENDERAL_PREFS_INI = IniRead($redirectorIni, "Redirection", "PrefsIni", "") $PLUGINS_FILE = IniRead($redirectorIni, "Redirection", "Plugins", "") ; Derive directories from file paths $DOCUMENTS_DIR = StringRegExpReplace($ENDERAL_INI, "\\[^\\]+$", "") $PLUGINS_DIR = StringRegExpReplace($PLUGINS_FILE, "\\[^\\]+$", "") Else ; SkyrimRedirector.dll exists without INI - create INI with Enderal defaults Local $baseDir = GetEnderalBaseDirectoryName() ; Set plugins directory path $PLUGINS_DIR = $LOCALAPPDATA & "\" & $baseDir $PLUGINS_FILE = $PLUGINS_DIR & "\plugins.txt" ; Get Documents folder path with Enderal INI names $DOCUMENTS_DIR = GetDocumentsPath() & "\My Games\" & $baseDir $ENDERAL_INI = $DOCUMENTS_DIR & "\Enderal.ini" $ENDERAL_PREFS_INI = $DOCUMENTS_DIR & "\EnderalPrefs.ini" ; Create SkyrimRedirector.ini with default paths IniWrite($redirectorIni, "Redirection", "Ini", $ENDERAL_INI) IniWrite($redirectorIni, "Redirection", "PrefsIni", $ENDERAL_PREFS_INI) IniWrite($redirectorIni, "Redirection", "Plugins", $PLUGINS_FILE) EndIf Else ; No SkyrimRedirector - use Skyrim default paths Local $baseDir = GetBaseDirectoryName() ; Set plugins directory path $PLUGINS_DIR = $LOCALAPPDATA & "\" & $baseDir $PLUGINS_FILE = $PLUGINS_DIR & "\plugins.txt" ; Get Documents folder path $DOCUMENTS_DIR = GetDocumentsPath() & "\My Games\" & $baseDir $ENDERAL_INI = $DOCUMENTS_DIR & "\Skyrim.ini" $ENDERAL_PREFS_INI = $DOCUMENTS_DIR & "\SkyrimPrefs.ini" EndIf ; Perform startup checks StartupChecks() ; Auto-start Steam if enabled (Steam version only) If $bIsSteamVersion Then Local $startSteam = RegRead($REG_KEY, $REG_START_STEAM) If Not @error And $startSteam = 1 Then ; Check if Steam is already running If Not ProcessExists("steam.exe") Then ShellExecute("steam://open/main") EndIf EndIf EndIf ; Initialize GDI+ _GDIPlus_Startup() ; Create and show main window CreateMainWindow() ; Create settings window (hidden) CreateSettingsWindow() ; Register message handler for dragging GUIRegisterMsg($WM_LBUTTONDOWN, "WM_LBUTTONDOWN") ; Register message handler for slider updates GUIRegisterMsg($WM_HSCROLL, "WM_HSCROLL") ; Register message handler for plugin list check boxes GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY") ; Main loop MainLoop() ; Cleanup _GDIPlus_Shutdown() EndFunc ; ============================================ ; Window Drag Handler ; ============================================ Func WM_LBUTTONDOWN($hWnd, $iMsg, $wParam, $lParam) If $hWnd = $hMainGUI Then _SendMessage($hMainGUI, $WM_SYSCOMMAND, 0xF012, 0) EndIf Return $GUI_RUNDEFMSG EndFunc Func WM_HSCROLL($hWnd, $iMsg, $wParam, $lParam) If $lParam = GUICtrlGetHandle($sldFramerateLimit) Then GUICtrlSetData($lblFramerateLimitValue, GUICtrlRead($sldFramerateLimit)) EndIf Return $GUI_RUNDEFMSG EndFunc Func WM_NOTIFY($hWnd, $iMsg, $wParam, $lParam) ; Handles the plugin list: check box changes send no GUIGetMsg event, and rows that ; cannot be changed are drawn dimmed If $hLvPlugins = 0 Then Return $GUI_RUNDEFMSG Local $tNMHDR = DllStructCreate($tagNMHDR, $lParam) If @error Then Return $GUI_RUNDEFMSG If HWnd(DllStructGetData($tNMHDR, "hWndFrom")) <> $hLvPlugins Then Return $GUI_RUNDEFMSG Switch DllStructGetData($tNMHDR, "Code") Case $NM_CUSTOMDRAW Return DrawPluginRow($lParam) Case $LVN_ITEMCHANGING, $LVN_ITEMCHANGED Return OnPluginStateChange(DllStructGetData($tNMHDR, "Code"), $lParam) EndSwitch Return $GUI_RUNDEFMSG EndFunc Func DrawPluginRow($lParam) Local $tCustomDraw = DllStructCreate($tagNMLVCUSTOMDRAW, $lParam) If @error Then Return $CDRF_DODEFAULT Switch DllStructGetData($tCustomDraw, "dwDrawStage") Case $CDDS_PREPAINT Return $CDRF_NOTIFYITEMDRAW Case $CDDS_ITEMPREPAINT Local $iItem = DllStructGetData($tCustomDraw, "dwItemSpec") If $iItem >= 0 And $iItem < $iPluginListCount And IsPluginRowFixed($iItem) Then DllStructSetData($tCustomDraw, "clrText", _WinAPI_GetSysColor($COLOR_GRAYTEXT)) EndIf Return $CDRF_NEWFONT EndSwitch Return $CDRF_DODEFAULT EndFunc Func OnPluginStateChange($iCode, $lParam) If $bPluginListUpdating Then Return $GUI_RUNDEFMSG Local $tNMLV = DllStructCreate($tagNMLISTVIEW, $lParam) If @error Then Return $GUI_RUNDEFMSG If Not BitAND(DllStructGetData($tNMLV, "Changed"), $LVIF_STATE) Then Return $GUI_RUNDEFMSG Local $iItem = DllStructGetData($tNMLV, "Item") Local $iOldCheck = BitShift(BitAND(DllStructGetData($tNMLV, "OldState"), $LVIS_STATEIMAGEMASK), 12) Local $iNewCheck = BitShift(BitAND(DllStructGetData($tNMLV, "NewState"), $LVIS_STATEIMAGEMASK), 12) ; A check state change has both state image indexes set Local $bCheckChanged = ($iOldCheck <> 0 And $iNewCheck <> 0 And $iOldCheck <> $iNewCheck) Local $bValidItem = ($iItem >= 0 And $iItem < $iPluginListCount) If $iCode = $LVN_ITEMCHANGING Then ; Rows that cannot be changed keep their state If $bCheckChanged And $bValidItem And IsPluginRowFixed($iItem) Then Return True Return $GUI_RUNDEFMSG EndIf If $bCheckChanged And $bValidItem Then If IsPluginRowFixed($iItem) Then ; Safety net in case the rejection above did not take effect $bPluginListUpdating = True SetPluginRowState($iItem) $bPluginListUpdating = False Else $aPluginList[$iItem][1] = ($iNewCheck = $PLUGIN_STATE_CHECKED) $bPluginListDirty = True EndIf EndIf UpdatePluginButtons() Return $GUI_RUNDEFMSG EndFunc ; ============================================ ; Startup Checks ; ============================================ Func StartupChecks() ; Check if Enderal is installed If Not FileExists($LAUNCHER_DIR & "\Data\Enderal - Forgotten Stories.esm") Then MsgBox(16, "Error", "Unable to find Enderal - Forgotten Stories.esm (Enderal SE is not installed?)") Exit EndIf If @OSVersion = 'WIN_10' Or @OSVersion = 'WIN_11' Then DllCall("User32.dll", "bool", "SetProcessDpiAwarenessContext" , "HWND", "DPI_AWARENESS_CONTEXT" -2) If @OSVersion = 'WIN_81' Then DllCall("User32.dll", "bool", "SetProcessDPIAware") ; Detect Steam version by checking for steam_api64.dll $bIsSteamVersion = FileExists($LAUNCHER_DIR & "\steam_api64.dll") ; Detect Mod Organizer 2 while its process is still our parent. ; Under MO2 the load order belongs to the active MO2 profile, so the launcher must not touch it. $MO2_DIR = GetMO2Directory() $bPluginListReadOnly = ($MO2_DIR <> "") CheckRegistryPaths() CheckPluginsFile() CheckINIFiles() CheckSaveDirectory() EndFunc Func CheckRegistryPaths() Local $installPathExpected = $LAUNCHER_DIR & "\" Local $exePathExpected = @ScriptFullPath ; Check and update Install_Path Local $installPath = RegRead($REG_KEY, $REG_INSTALL_PATH) If @error Or $installPath <> $installPathExpected Then RegWrite($REG_KEY, $REG_INSTALL_PATH, "REG_SZ", $installPathExpected) EndIf ; Check and update Exe_Path Local $exePath = RegRead($REG_KEY, $REG_EXE_PATH) If @error Or $exePath <> $exePathExpected Then RegWrite($REG_KEY, $REG_EXE_PATH, "REG_SZ", $exePathExpected) EndIf ; For Steam version: initialize Start_Steam if it doesn't exist If $bIsSteamVersion Then Local $startSteam = RegRead($REG_KEY, $REG_START_STEAM) If @error Then RegWrite($REG_KEY, $REG_START_STEAM, "REG_DWORD", 1) EndIf EndIf EndFunc Func GetRequiredPlugins() ; Plugins Enderal cannot run without - always kept enabled Local $aRequired[2] = ["Enderal - Forgotten Stories.esm", "SkyUI_SE.esp"] Return $aRequired EndFunc Func CheckPluginsFile() Local $aRequired = GetRequiredPlugins() Local $requiredLines[UBound($aRequired)] Local $iRequired = 0 ; Only enforce required plugins that are installed (SkyUI may be absent) For $i = 0 To UBound($aRequired) - 1 If Not FileExists($LAUNCHER_DIR & "\Data\" & $aRequired[$i]) Then ContinueLoop $requiredLines[$iRequired] = "*" & $aRequired[$i] $iRequired += 1 Next ReDim $requiredLines[$iRequired] If Not DirExists($PLUGINS_DIR) Then DirCreate($PLUGINS_DIR) EndIf Local $fileContent = "" If FileExists($PLUGINS_FILE) Then $fileContent = FileRead($PLUGINS_FILE) EndIf Local $modified = False Local $lines = StringSplit($fileContent, @CRLF, 1) For $i = 0 To UBound($requiredLines) - 1 Local $lineWithAsterisk = $requiredLines[$i] Local $lineWithoutAsterisk = StringTrimLeft($lineWithAsterisk, 1) Local $foundWithAsterisk = False Local $foundWithoutAsterisk = False Local $lineIndex = -1 For $j = 1 To $lines[0] Local $trimmedLine = StringStripWS($lines[$j], 3) If $trimmedLine = $lineWithAsterisk Then $foundWithAsterisk = True ExitLoop ElseIf $trimmedLine = $lineWithoutAsterisk Then $foundWithoutAsterisk = True $lineIndex = $j ExitLoop EndIf Next If $foundWithAsterisk Then ContinueLoop ElseIf $foundWithoutAsterisk Then $lines[$lineIndex] = $lineWithAsterisk $modified = True Else If $fileContent <> "" And StringRight($fileContent, 2) <> @CRLF Then $fileContent &= @CRLF EndIf $fileContent &= $lineWithAsterisk & @CRLF $modified = True ContinueLoop EndIf Next If $modified Then Local $newContent = "" For $j = 1 To $lines[0] $newContent &= $lines[$j] If $j < $lines[0] Then $newContent &= @CRLF Next If StringInStr($fileContent, "*Enderal - Forgotten Stories.esm") Or StringInStr($fileContent, "*SkyUI_SE.esp") Then $newContent = $fileContent EndIf Local $hFile = FileOpen($PLUGINS_FILE, 2) FileWrite($hFile, $newContent) FileClose($hFile) EndIf EndFunc Func GetPhysicalResolution() ; Get physical screen resolution using EnumDisplaySettingsW ; This bypasses DPI scaling which affects @DesktopWidth/@DesktopHeight Local Const $DEVMODE_SIZE = 220 Local Const $OFFSET_dmPelsWidth = 172 Local Const $OFFSET_dmPelsHeight = 176 Local Const $ENUM_CURRENT_SETTINGS = -1 Local $tDevMode = DllStructCreate("byte[" & $DEVMODE_SIZE & "]") DllStructSetData($tDevMode, 1, $DEVMODE_SIZE, 68) ; dmSize at offset 68 Local $aResult = DllCall("user32.dll", "bool", "EnumDisplaySettingsW", _ "ptr", 0, _ "dword", $ENUM_CURRENT_SETTINGS, _ "struct*", $tDevMode) If @error Or Not $aResult[0] Then ; Fallback to AutoIt macros if DllCall fails Local $aRes[2] = [@DesktopWidth, @DesktopHeight] Return $aRes EndIf Local $width = DllStructGetData(DllStructCreate("dword", DllStructGetPtr($tDevMode) + $OFFSET_dmPelsWidth), 1) Local $height = DllStructGetData(DllStructCreate("dword", DllStructGetPtr($tDevMode) + $OFFSET_dmPelsHeight), 1) Local $aRes[2] = [$width, $height] Return $aRes EndFunc Func CheckINIFiles() If Not DirExists($DOCUMENTS_DIR) Then DirCreate($DOCUMENTS_DIR) EndIf Local $iniExists = FileExists($ENDERAL_INI) Local $prefsExists = FileExists($ENDERAL_PREFS_INI) If Not $iniExists Or Not $prefsExists Then Local $aPhysicalRes = GetPhysicalResolution() Local $screenWidth = $aPhysicalRes[0] Local $screenHeight = $aPhysicalRes[1] If Not $iniExists Then Local $defaultIni = $LAUNCHER_DIR & "\Enderal_default.ini" If Not FileExists($defaultIni) Then $defaultIni = $LAUNCHER_DIR & "\Skyrim_default.ini" If FileExists($defaultIni) Then FileCopy($defaultIni, $ENDERAL_INI) EndIf If Not $prefsExists Then Local $defaultPrefsIni = $LAUNCHER_DIR & "\EnderalPrefs_default.ini" If Not FileExists($defaultPrefsIni) Then $defaultPrefsIni = $LAUNCHER_DIR & "\SkyrimPrefs_default.ini" If FileExists($defaultPrefsIni) Then FileCopy($defaultPrefsIni, $ENDERAL_PREFS_INI) IniWrite($ENDERAL_PREFS_INI, "Display", "iSize W", $screenWidth) IniWrite($ENDERAL_PREFS_INI, "Display", "iSize H", $screenHeight) EndIf EndIf Else Local $shadowValue = IniRead($ENDERAL_PREFS_INI, "Display", "iShadowMaskQuarter", "") If $shadowValue <> "4" Then IniWrite($ENDERAL_PREFS_INI, "Display", "iShadowMaskQuarter", "4") EndIf EndIf EndFunc Func CheckSaveDirectory() ; Read sLocalSavePath from Enderal.ini (or custom INI) and ensure the directory exists Local $savePath = IniReadWithCustom("General", "sLocalSavePath", "") ; Save path is relative to Skyrim's documents directory Local $skyrimDocsDir = GetDocumentsPath() & "\My Games\" & GetBaseDirectoryName() ; Fix empty or legacy relative paths that start with ".." If $savePath = "" Or StringLeft($savePath, 2) = ".." Then Local $newSavePath = "EnderalSaves\" Local $newFullPath = $skyrimDocsDir & "\" & $newSavePath ; Ensure new directory exists and update INI If Not DirExists($newFullPath) Then DirCreate($newFullPath) EndIf IniWriteWithCustom("General", "sLocalSavePath", $newSavePath) EndIf EndFunc Func GetDocumentsPath() Local $oShell = ObjCreate("Shell.Application") If IsObj($oShell) Then Local $oFolder = $oShell.Namespace(0x05) If IsObj($oFolder) Then Return $oFolder.Self.Path EndIf EndIf Return @MyDocumentsDir EndFunc ; ============================================ ; Main Window ; ============================================ Func CreateMainWindow() $hMainGUI = GUICreate("Enderal Launcher", $LAUNCHER_WIDTH, $LAUNCHER_HEIGHT, -1, -1, $WS_POPUP + $WS_CLIPCHILDREN) GUISetIcon($LAUNCHER_DIR & "\enderal.ico") ;GUISetBkColor(0x1A1A1A, $hMainGUI) ; Set background image SetBackgroundImage() ; Create top link bar on the left Local $linkBarY = 10 Local $linkSpacing = 80 Local $startX = 20 ; Credits - shadow first, then main label $lblCreditsShadow = GUICtrlCreateLabel("Credits", $startX + 1, $linkBarY + 1, 70, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) $lblCredits = GUICtrlCreateLabel("Credits", $startX, $linkBarY, 70, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) ; Wiki - shadow first, then main label $lblWikiShadow = GUICtrlCreateLabel("Wiki", $startX + $linkSpacing + 1, $linkBarY + 1, 70, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) $lblWiki = GUICtrlCreateLabel("Wiki", $startX + $linkSpacing, $linkBarY, 70, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) ; Discord - shadow first, then main label $lblSupportShadow = GUICtrlCreateLabel("Discord", $startX + $linkSpacing * 2 + 1, $linkBarY + 1, 70, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) $lblSupport = GUICtrlCreateLabel("Discord", $startX + $linkSpacing * 2, $linkBarY, 70, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) ; Changelog - shadow first, then main label $lblChangelogShadow = GUICtrlCreateLabel("Changelog", $startX + $linkSpacing * 3 + 9 + 1, $linkBarY + 1, 100, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) $lblChangelog = GUICtrlCreateLabel("Changelog", $startX + $linkSpacing * 3 + 9, $linkBarY, 100, 25, $SS_CENTER) GUICtrlSetFont(-1, 10, 400, 4) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) ; Create close and minimize buttons at top right ; Minimize - shadow first, then main label $lblMinimizeShadow = GUICtrlCreateLabel("_", $LAUNCHER_WIDTH - 60 + 1, 5 + 1, 25, 25, $SS_CENTER) GUICtrlSetFont(-1, 14, 700) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) $btnMinimize = GUICtrlCreateLabel("_", $LAUNCHER_WIDTH - 60, 5, 25, 25, $SS_CENTER) GUICtrlSetFont(-1, 14, 700) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) ; Close - shadow first, then main label $lblCloseShadow = GUICtrlCreateLabel("X", $LAUNCHER_WIDTH - 30 + 1, 5 + 1, 25, 25, $SS_CENTER) GUICtrlSetFont(-1, 12, 700) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) $btnClose = GUICtrlCreateLabel("X", $LAUNCHER_WIDTH - 30, 5, 25, 25, $SS_CENTER) GUICtrlSetFont(-1, 12, 700) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUICtrlSetCursor(-1, 0) ; Create main buttons Local $sTempDir = @TempDir & "\EnderalLauncher\" ; Create a unique temp folder DirCreate($sTempDir) Local $btnWidth = 200 Local $btnHeight = 82 Local $btnX = $LAUNCHER_WIDTH - $btnWidth - 60 Local $btnStartY = $LAUNCHER_HEIGHT - 420 Local $sDestinationFile = $sTempDir & "play.bmp" FileInstall("play.bmp", $sDestinationFile, 1) $btnPlay = GUICtrlCreateButton("PLAY", $btnX, $btnStartY, $btnWidth, $btnHeight, $BS_BITMAP) GUICtrlSetFont(-1, 12, 400, $GUI_FONTNORMAL, "Tahoma", $CLEARTYPE_QUALITY) GUICtrlSetImage($btnPlay, $sDestinationFile) GUICtrlSetCursor($btnPlay, 0) $sDestinationFile = $sTempDir & "settings.bmp" FileInstall("settings.bmp", $sDestinationFile, 1) $btnSettings = GUICtrlCreateButton("Settings", $btnX, $btnStartY + 110, $btnWidth, $btnHeight, $BS_BITMAP) GUICtrlSetFont(-1, 12, 400, $GUI_FONTNORMAL, "Tahoma", $CLEARTYPE_QUALITY) GUICtrlSetImage($btnSettings, $sDestinationFile) GUICtrlSetCursor($btnSettings, 0) $sDestinationFile = $sTempDir & "exit.bmp" FileInstall("exit.bmp", $sDestinationFile, 1) $btnExit = GUICtrlCreateButton("Exit", $btnX, $btnStartY + 220, $btnWidth, $btnHeight, $BS_BITMAP) GUICtrlSetFont(-1, 12, 400, $GUI_FONTNORMAL, "Tahoma", $CLEARTYPE_QUALITY) GUICtrlSetImage($btnExit, $sDestinationFile) GUICtrlSetCursor($btnExit, 0) ; Create bottom version bar CreateVersionBar() GUISetState(@SW_SHOW, $hMainGUI) EndFunc Func CreateVersionBar() Local $bottomY = $LAUNCHER_HEIGHT - 30 Local $fontSize = 9 ; Get versions Local $skyrimVer = GetSkyrimVersion() Local $enderalVer = GetEnderalVersion() ; Determine display text for each version (include "v" only when version is found) Local $skyrimText = "N/A" If $skyrimVer <> "" Then $skyrimText = "v" & $skyrimVer Local $enderalText = "N/A" If $enderalVer <> "" Then $enderalText = "v" & $enderalVer ; Build full version string Local $fullText = "Skyrim SE " & $skyrimText & " | Enderal SE " & $enderalText ; Center the text using a wide label with center alignment Local $labelWidth = 500 Local $startX = ($LAUNCHER_WIDTH - $labelWidth) / 2 ; Shadow label GUICtrlCreateLabel($fullText, $startX + 1, $bottomY + 1, $labelWidth, 20, $SS_CENTER) GUICtrlSetFont(-1, $fontSize, 400) GUICtrlSetColor(-1, 0x000000) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) ; Main label GUICtrlCreateLabel($fullText, $startX, $bottomY, $labelWidth, 20, $SS_CENTER) GUICtrlSetFont(-1, $fontSize, 400) GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) EndFunc Func SetBackgroundImage() ; Extract background from embedded resource to temp Local $sTempDir = @TempDir & "\EnderalLauncher\" DirCreate($sTempDir) Local $bgFile = $sTempDir & "background.jpg" FileInstall("background.jpg", $bgFile, 1) If FileExists($bgFile) Then Local $hImage = _GDIPlus_ImageLoadFromFile($bgFile) If $hImage Then Local $hGraphic = _GDIPlus_GraphicsCreateFromHWND($hMainGUI) Local $hBitmap = _GDIPlus_BitmapCreateFromGraphics($LAUNCHER_WIDTH, $LAUNCHER_HEIGHT, $hGraphic) Local $hBuffer = _GDIPlus_ImageGetGraphicsContext($hBitmap) ; Get original image dimensions Local $imgWidth = _GDIPlus_ImageGetWidth($hImage) Local $imgHeight = _GDIPlus_ImageGetHeight($hImage) ; Calculate scale factor to cover the entire window (crop to fit) Local $scaleX = $LAUNCHER_WIDTH / $imgWidth Local $scaleY = $LAUNCHER_HEIGHT / $imgHeight Local $scale = ($scaleX > $scaleY) ? $scaleX : $scaleY ; Calculate scaled dimensions Local $scaledWidth = $imgWidth * $scale Local $scaledHeight = $imgHeight * $scale ; Calculate offset to center the image (crop from center) Local $offsetX = ($LAUNCHER_WIDTH - $scaledWidth) / 2 Local $offsetY = ($LAUNCHER_HEIGHT - $scaledHeight) / 2 ; Draw image with aspect ratio preserved, centered and cropped _GDIPlus_GraphicsDrawImageRect($hBuffer, $hImage, $offsetX, $offsetY, $scaledWidth, $scaledHeight) Local $hGDIBitmap = _GDIPlus_BitmapCreateHBITMAPFromBitmap($hBitmap) $hBackgroundPic = GUICtrlCreatePic("", 0, 0, $LAUNCHER_WIDTH, $LAUNCHER_HEIGHT) _SetBitmapToCtrl($hBackgroundPic, $hGDIBitmap) GUICtrlSetState($hBackgroundPic, $GUI_DISABLE) _WinAPI_DeleteObject($hGDIBitmap) _GDIPlus_GraphicsDispose($hBuffer) _GDIPlus_BitmapDispose($hBitmap) _GDIPlus_GraphicsDispose($hGraphic) _GDIPlus_ImageDispose($hImage) EndIf EndIf EndFunc Func _SetBitmapToCtrl($idPic, $hBitmap) Local $hWnd = GUICtrlGetHandle($idPic) _SendMessage($hWnd, 0x0172, 0, $hBitmap) EndFunc ; ============================================ ; Settings Window (child window) ; ============================================ Func CreateSettingsWindow() ; Create settings as a child window of main window $hSettingsGUI = GUICreate("Settings", $SETTINGS_WIDTH, $SETTINGS_HEIGHT, _ $SETTINGS_MARGIN, $SETTINGS_MARGIN, $WS_POPUP, $WS_EX_MDICHILD, $hMainGUI) ;GUISetBkColor(0xF0F0F0, $hSettingsGUI) ; Tab control $tabSettings = GUICtrlCreateTab(15, 20, $SETTINGS_WIDTH - 30, $SETTINGS_HEIGHT - 80) ; Display tab (includes rendering controls on right side) GUICtrlCreateTabItem("Display") CreateDisplayControls() CreateRenderingControls() ; General tab GUICtrlCreateTabItem("General") CreateControlsControls() ; Plugins tab GUICtrlCreateTabItem("Plugins") CreatePluginsControls() ; End tab items GUICtrlCreateTabItem("") ; Save and Cancel buttons (outside tabs) $btnSave = GUICtrlCreateButton("Save", $SETTINGS_WIDTH - 205, $SETTINGS_HEIGHT - 50, 90, 35) $btnCancel = GUICtrlCreateButton("Cancel", $SETTINGS_WIDTH - 105, $SETTINGS_HEIGHT - 50, 90, 35) ; Hide initially GUISetState(@SW_HIDE, $hSettingsGUI) ; Switch back to main GUI GUISwitch($hMainGUI) EndFunc Func CreateDisplayControls() Local $leftMargin = $TAB_LEFT_MARGIN Local $topMargin = $TAB_TOP_MARGIN Local $rowHeight = $TAB_ROW_HEIGHT ; Resolution $lblResolution = GUICtrlCreateLabel("Resolution:", $leftMargin, $topMargin, 100, 20) $cmbResolution = GUICtrlCreateCombo("", $leftMargin + 110, $topMargin - 3, 200, 200, $CBS_DROPDOWNLIST) PopulateResolutions() ; Antialiasing $lblAntialiasing = GUICtrlCreateLabel("Antialiasing:", $leftMargin, $topMargin + $rowHeight, 100, 20) $cmbAntialiasing = GUICtrlCreateCombo("", $leftMargin + 110, $topMargin + $rowHeight - 3, 200, 200, $CBS_DROPDOWNLIST) GUICtrlSetData($cmbAntialiasing, $AA_TAA & "|" & $AA_FXAA & "|" & $AA_OFF) ; Windowed Mode $chkWindowed = GUICtrlCreateCheckbox("Windowed Mode", $leftMargin, $topMargin + $rowHeight * 2, 150, 20) ; Detail group Local $columnWidth = 370 GUICtrlCreateGroup("Detail Presets", $leftMargin, $topMargin + $rowHeight * 3, $columnWidth, 80) Local $detailBtnY = $topMargin + $rowHeight * 3 + 30 Local $detailBtnWidth = 85 Local $detailBtnHeight = 32 $btnLow = GUICtrlCreateButton("Low", $leftMargin + 8, $detailBtnY, $detailBtnWidth, $detailBtnHeight) $btnMedium = GUICtrlCreateButton("Medium", $leftMargin + 95, $detailBtnY, $detailBtnWidth, $detailBtnHeight) $btnHigh = GUICtrlCreateButton("High", $leftMargin + 182, $detailBtnY, $detailBtnWidth, $detailBtnHeight) $btnUltra = GUICtrlCreateButton("Ultra", $leftMargin + 269, $detailBtnY, $detailBtnWidth, $detailBtnHeight) GUICtrlCreateGroup("", -99, -99, 1, 1) ; SSE Display Tweaks group Local $sseGroupY = $topMargin + $rowHeight * 3 + 100 $grpSSEDisplayTweaks = GUICtrlCreateGroup("SSE Display Tweaks", $leftMargin, $sseGroupY, $columnWidth, 90) $chkSSEDisplayTweaks = GUICtrlCreateCheckbox("Enable", $leftMargin + 10, $sseGroupY + 25, 200, 20) GUICtrlSetTip(-1, "Enables support for refresh rates higher than 60 Hz.") $lblFramerateLimit = GUICtrlCreateLabel("Framerate limit:", $leftMargin + 10, $sseGroupY + 55, 110, 20) $sldFramerateLimit = GUICtrlCreateSlider($leftMargin + 130, $sseGroupY + 50, 185, 25) GUICtrlSetLimit($sldFramerateLimit, @DesktopRefresh, 30) $lblFramerateLimitValue = GUICtrlCreateLabel("60", $leftMargin + 320, $sseGroupY + 55, 35, 20) GUICtrlCreateGroup("", -99, -99, 1, 1) EndFunc Func CreateRenderingControls() ; Position on right side of Display tab Local $leftMargin = $TAB_LEFT_MARGIN + 370 + 40 Local $topMargin = $TAB_TOP_MARGIN Local $rowHeight = $TAB_ROW_HEIGHT Local $columnWidth = 370 $chk64bitRT = GUICtrlCreateCheckbox("64-bit RenderTarget", $leftMargin, $topMargin, $columnWidth - 20, 20) GUICtrlSetTip(-1, "Uses 64-bit precision for HDR rendering instead of 32-bit." & @CRLF & "Reduces color banding in skies and gradients. Uses more VRAM.") $chkSSAO = GUICtrlCreateCheckbox("Screen Space Ambient Occlusion", $leftMargin, $topMargin + $rowHeight, $columnWidth - 20, 20) GUICtrlSetTip(-1, "Adds soft shadows in corners and where objects meet surfaces." & @CRLF & "Makes scenes look more realistic. Has a performance cost.") $chkVolumetricLighting = GUICtrlCreateCheckbox("Volumetric Lighting", $leftMargin, $topMargin + $rowHeight * 2, $columnWidth - 20, 20) GUICtrlSetTip(-1, "Adds visible light rays (god rays) through fog and atmosphere." & @CRLF & "Creates atmospheric sunbeams. Has a performance cost.") $chkSSR = GUICtrlCreateCheckbox("Screen Space Reflections", $leftMargin, $topMargin + $rowHeight * 3, $columnWidth - 20, 20) GUICtrlSetTip(-1, "Skyrim SE is affected by the green water bug with this option enabled." & @CRLF & "Recommended to enable only with ENB or Community Shaders.") EndFunc Func CreateControlsControls() Local $leftMargin = $TAB_LEFT_MARGIN Local $topMargin = $TAB_TOP_MARGIN Local $rowHeight = $TAB_ROW_HEIGHT Local $columnWidth = 370 Local $rightMargin = $leftMargin + $columnWidth + 20 ; Controls group GUICtrlCreateGroup("Controls", $leftMargin, $topMargin, $columnWidth, 80) $chkController = GUICtrlCreateCheckbox("Enable Controller", $leftMargin + 10, $topMargin + 25, $columnWidth - 20, 20) $chkVibration = GUICtrlCreateCheckbox("Controller Vibration", $leftMargin + 10, $topMargin + 50, $columnWidth - 20, 20) GUICtrlCreateGroup("", -99, -99, 1, 1) ; Steam-only: Start Steam checkbox $chkStartSteam = GUICtrlCreateCheckbox("Start Steam on launcher start", $leftMargin, $topMargin + 95, $columnWidth, 20) If Not $bIsSteamVersion Then GUICtrlSetState($chkStartSteam, $GUI_HIDE) EndIf ; INI Files group (right side) $grpIniFiles = GUICtrlCreateGroup("INI Files", $rightMargin, $topMargin, $columnWidth, 313) Local $iniButtonWidth = $columnWidth - 30 Local $iniButtonHeight = 44 Local $iniButtonSpacing = 54 $btnOpenEnderalIni = GUICtrlCreateButton(GetIniFileName($ENDERAL_INI), $rightMargin + 15, $topMargin + 28, $iniButtonWidth, $iniButtonHeight) $btnOpenEnderalPrefsIni = GUICtrlCreateButton(GetIniFileName($ENDERAL_PREFS_INI), $rightMargin + 15, $topMargin + 28 + $iniButtonSpacing, $iniButtonWidth, $iniButtonHeight) $btnOpenCustomIni = GUICtrlCreateButton(GetCustomIniFileName(), $rightMargin + 15, $topMargin + 28 + $iniButtonSpacing * 2, $iniButtonWidth, $iniButtonHeight) UpdateCustomIniButtonState() $btnOpenModIni = GUICtrlCreateButton(GetModIniFileName(), $rightMargin + 15, $topMargin + 28 + $iniButtonSpacing * 3, $iniButtonWidth, $iniButtonHeight) UpdateModIniButtonState() $btnOpenSavegames = GUICtrlCreateButton("Saved games directory", $rightMargin + 15, $topMargin + 28 + $iniButtonSpacing * 4, $iniButtonWidth, $iniButtonHeight) GUICtrlCreateGroup("", -99, -99, 1, 1) EndFunc Func CreatePluginsControls() Local $leftMargin = $TAB_LEFT_MARGIN Local $topMargin = 58 Local $listWidth = 560 Local $listHeight = 340 Local $sideX = $leftMargin + $listWidth + 20 Local $sideWidth = 198 Local $btnHeight = 32 $lvPlugins = GUICtrlCreateListView("Plugin|Type", $leftMargin, $topMargin, $listWidth, $listHeight, _ BitOR($LVS_REPORT, $LVS_SINGLESEL, $LVS_SHOWSELALWAYS, $LVS_NOSORTHEADER), _ BitOR($LVS_EX_CHECKBOXES, $LVS_EX_FULLROWSELECT, $LVS_EX_DOUBLEBUFFER)) $hLvPlugins = GUICtrlGetHandle($lvPlugins) _GUICtrlListView_SetColumnWidth($hLvPlugins, 0, 420) _GUICtrlListView_SetColumnWidth($hLvPlugins, 1, 120) CreatePluginStateImages() $btnPluginUp = GUICtrlCreateButton("Move Up", $sideX, $topMargin, $sideWidth, $btnHeight) GUICtrlSetState(-1, $GUI_DISABLE) $btnPluginDown = GUICtrlCreateButton("Move Down", $sideX, $topMargin + 38, $sideWidth, $btnHeight) GUICtrlSetState(-1, $GUI_DISABLE) $btnPluginsEnableAll = GUICtrlCreateButton("Enable All", $sideX, $topMargin + 90, $sideWidth, $btnHeight) $btnPluginsDisableAll = GUICtrlCreateButton("Disable All", $sideX, $topMargin + 128, $sideWidth, $btnHeight) $lblPluginsCount = GUICtrlCreateLabel("", $sideX, $topMargin + 178, $sideWidth, 20) Local $hint = "Masters (ESM/ESL) always load before regular plugins." & @CRLF & @CRLF & _ "Plugins Enderal requires are loaded by the game itself and cannot be changed." If $bPluginListReadOnly Then $hint = "The load order belongs to the active Mod Organizer profile." & @CRLF & @CRLF & _ "Enable, disable and sort plugins in Mod Organizer instead." EndIf $lblPluginsHint = GUICtrlCreateLabel($hint, $sideX, $topMargin + 206, $sideWidth, 134) GUICtrlSetFont(-1, 8) GUICtrlSetColor(-1, 0x606060) EndFunc Func PopulateResolutions() Local $aResolutions = GetSupportedResolutions() Local $resolutions = "" For $i = 0 To UBound($aResolutions) - 1 If $i > 0 Then $resolutions &= "|" $resolutions &= $aResolutions[$i] Next GUICtrlSetData($cmbResolution, $resolutions) EndFunc Func GetSupportedResolutions() ; DEVMODEW structure size and offsets Local Const $DEVMODE_SIZE = 220 Local Const $OFFSET_dmPelsWidth = 172 Local Const $OFFSET_dmPelsHeight = 176 Local $tDevMode = DllStructCreate("byte[" & $DEVMODE_SIZE & "]") DllStructSetData($tDevMode, 1, $DEVMODE_SIZE, 68) ; dmSize at offset 68 Local $aTemp[100][2] ; Temporary array for width, height pairs Local $count = 0 Local $iModeNum = 0 ; Enumerate all display modes While 1 Local $aResult = DllCall("user32.dll", "bool", "EnumDisplaySettingsW", _ "ptr", 0, _ "dword", $iModeNum, _ "struct*", $tDevMode) If @error Or Not $aResult[0] Then ExitLoop Local $width = DllStructGetData(DllStructCreate("dword", DllStructGetPtr($tDevMode) + $OFFSET_dmPelsWidth), 1) Local $height = DllStructGetData(DllStructCreate("dword", DllStructGetPtr($tDevMode) + $OFFSET_dmPelsHeight), 1) ; Check for duplicate resolutions Local $isDuplicate = False For $i = 0 To $count - 1 If $aTemp[$i][0] = $width And $aTemp[$i][1] = $height Then $isDuplicate = True ExitLoop EndIf Next If Not $isDuplicate And $width >= 800 And $height >= 600 Then If $count >= UBound($aTemp) Then ReDim $aTemp[$count + 50][2] EndIf $aTemp[$count][0] = $width $aTemp[$count][1] = $height $count += 1 EndIf $iModeNum += 1 WEnd ; Sort by total pixels (width * height) descending For $i = 0 To $count - 2 For $j = $i + 1 To $count - 1 Local $pixelsI = $aTemp[$i][0] * $aTemp[$i][1] Local $pixelsJ = $aTemp[$j][0] * $aTemp[$j][1] If $pixelsJ > $pixelsI Then Local $tmpW = $aTemp[$i][0] Local $tmpH = $aTemp[$i][1] $aTemp[$i][0] = $aTemp[$j][0] $aTemp[$i][1] = $aTemp[$j][1] $aTemp[$j][0] = $tmpW $aTemp[$j][1] = $tmpH EndIf Next Next ; Build result array Local $aResult[$count] For $i = 0 To $count - 1 $aResult[$i] = $aTemp[$i][0] & "x" & $aTemp[$i][1] Next Return $aResult EndFunc ; ============================================ ; Plugin Management (plugins.txt) ; ============================================ Func GetPluginHeaderFlags($filePath) ; Reads the record flags of the plugin's TES4 header Local $hFile = FileOpen($filePath, $FO_BINARY) If $hFile = -1 Then Return 0 Local $header = FileRead($hFile, 12) FileClose($hFile) If BinaryLen($header) < 12 Then Return 0 If BinaryToString(BinaryMid($header, 1, 4)) <> "TES4" Then Return 0 ; Record flags are a little endian dword at offset 8 Local $hex = StringTrimLeft(String(BinaryMid($header, 9, 4)), 2) Return Dec(StringMid($hex, 7, 2) & StringMid($hex, 5, 2) & StringMid($hex, 3, 2) & StringMid($hex, 1, 2), 2) EndFunc Func GetPluginTypeInfo($filePath, $fileName) ; Returns [load order class, type label] Local $ext = StringLower(StringRight($fileName, 4)) Local $flags = GetPluginHeaderFlags($filePath) Local $bMaster = BitAND($flags, $PLUGIN_FLAG_MASTER) <> 0 Or $ext = ".esm" Or $ext = ".esl" Local $bLight = BitAND($flags, $PLUGIN_FLAG_LIGHT) <> 0 Or $ext = ".esl" Local $aInfo[2] $aInfo[0] = $bMaster ? $PLUGIN_CLASS_MASTER : $PLUGIN_CLASS_NORMAL If $bLight Then $aInfo[1] = "Light" ElseIf $bMaster Then $aInfo[1] = "Master" Else $aInfo[1] = "Plugin" EndIf Return $aInfo EndFunc Func ScanDataPlugins() ; All plugin files present in the Data directory, sorted by name ; [i][0] = file name, [i][1] = load order class, [i][2] = type label Local $dataDir = $LAUNCHER_DIR & "\Data" Local $aFiles[64][3] Local $iCount = 0 Local $hSearch = FileFindFirstFile($dataDir & "\*.es*") If $hSearch <> -1 Then While 1 Local $fileName = FileFindNextFile($hSearch) If @error Then ExitLoop If @extended Then ContinueLoop ; skip directories Local $ext = StringLower(StringRight($fileName, 4)) If $ext <> ".esm" And $ext <> ".esp" And $ext <> ".esl" Then ContinueLoop If $iCount >= UBound($aFiles) Then ReDim $aFiles[$iCount + 64][3] Local $aInfo = GetPluginTypeInfo($dataDir & "\" & $fileName, $fileName) $aFiles[$iCount][0] = $fileName $aFiles[$iCount][1] = $aInfo[0] $aFiles[$iCount][2] = $aInfo[1] $iCount += 1 WEnd FileClose($hSearch) EndIf ReDim $aFiles[$iCount][3] If $iCount > 1 Then _ArraySort($aFiles, 0, 0, 0, 0) Return $aFiles EndFunc Func ReadPluginsFile() ; plugins.txt entries in file order: [i][0] = file name, [i][1] = enabled Local $aListed[1][2] Local $iCount = 0 If FileExists($PLUGINS_FILE) Then Local $hFile = FileOpen($PLUGINS_FILE, BitOR($FO_READ, $FO_UTF8)) If $hFile <> -1 Then Local $content = FileRead($hFile) FileClose($hFile) ; Drop a byte order mark left over from other tools If StringLeft($content, 1) = ChrW(65279) Then $content = StringTrimLeft($content, 1) Local $aLines = StringSplit(StringStripCR($content), @LF) ReDim $aListed[$aLines[0] + 1][2] For $i = 1 To $aLines[0] Local $line = StringStripWS($aLines[$i], 3) If $line = "" Or StringLeft($line, 1) = "#" Then ContinueLoop Local $bEnabled = (StringLeft($line, 1) = "*") If $bEnabled Then $line = StringStripWS(StringTrimLeft($line, 1), 3) If $line = "" Then ContinueLoop $aListed[$iCount][0] = $line $aListed[$iCount][1] = $bEnabled $iCount += 1 Next EndIf EndIf ReDim $aListed[$iCount][2] Return $aListed EndFunc Func GetCCCPlugins() ; Skyrim.ccc entries - the game loads these on its own whenever the files exist Local $aEntries[1] Local $iCount = 0 Local $cccFile = $LAUNCHER_DIR & "\Skyrim.ccc" If FileExists($cccFile) Then Local $aLines = StringSplit(StringStripCR(FileRead($cccFile)), @LF) ReDim $aEntries[$aLines[0] + 1] For $i = 1 To $aLines[0] Local $line = StringStripWS($aLines[$i], 3) If $line = "" Or StringLeft($line, 1) = "#" Then ContinueLoop $aEntries[$iCount] = $line $iCount += 1 Next EndIf ReDim $aEntries[$iCount] Return $aEntries EndFunc Func GetImplicitPlugins() ; Plugins the game loads by itself, in the order it loads them: the Skyrim masters, ; the Skyrim.ccc entries, then the plugins Enderal requires Local $aMasters[5] = ["Skyrim.esm", "Update.esm", "Dawnguard.esm", "HearthFires.esm", "Dragonborn.esm"] Local $aCCC = GetCCCPlugins() Local $aRequired = GetRequiredPlugins() Local $aImplicit[UBound($aMasters) + UBound($aCCC) + UBound($aRequired)] Local $iCount = 0 For $i = 0 To UBound($aMasters) - 1 $aImplicit[$iCount] = $aMasters[$i] $iCount += 1 Next For $i = 0 To UBound($aCCC) - 1 $aImplicit[$iCount] = $aCCC[$i] $iCount += 1 Next For $i = 0 To UBound($aRequired) - 1 $aImplicit[$iCount] = $aRequired[$i] $iCount += 1 Next Return $aImplicit EndFunc Func IsRequiredPlugin($fileName) Local $aRequired = GetRequiredPlugins() For $i = 0 To UBound($aRequired) - 1 If $aRequired[$i] = $fileName Then Return True Next Return False EndFunc Func FindPluginFile(ByRef $aFiles, $fileName) For $i = 0 To UBound($aFiles) - 1 If $aFiles[$i][0] = $fileName Then Return $i Next Return -1 EndFunc Func FindListedPlugin(ByRef $aListed, $fileName) For $i = 0 To UBound($aListed) - 1 If $aListed[$i][0] = $fileName Then Return $i Next Return -1 EndFunc Func FindPluginRow(ByRef $aList, $iCount, $fileName) For $i = 0 To $iCount - 1 If $aList[$i][0] = $fileName Then Return $i Next Return -1 EndFunc Func AddPluginRow(ByRef $aList, ByRef $iCount, ByRef $aFiles, $iFile, $bEnabled, $bLocked, $bListed) $aList[$iCount][0] = $aFiles[$iFile][0] $aList[$iCount][1] = $bEnabled $aList[$iCount][2] = $bLocked $aList[$iCount][3] = $aFiles[$iFile][1] $aList[$iCount][4] = $bLocked ? $aFiles[$iFile][2] & " (fixed)" : $aFiles[$iFile][2] $aList[$iCount][5] = $bListed $iCount += 1 EndFunc Func LoadPluginList() ; Keep unsaved changes when the settings are reloaded (e.g. after applying a detail preset) If $bPluginListDirty Then Return Local $aFiles = ScanDataPlugins() Local $aListed = ReadPluginsFile() Local $aImplicit = GetImplicitPlugins() Local $iFileCount = UBound($aFiles) Local $aList[$iFileCount + 1][6] Local $iCount = 0 Local $iFile ; Plugins loaded by the game itself come first and cannot be changed For $i = 0 To UBound($aImplicit) - 1 $iFile = FindPluginFile($aFiles, $aImplicit[$i]) If $iFile < 0 Then ContinueLoop ; not installed If FindPluginRow($aList, $iCount, $aFiles[$iFile][0]) >= 0 Then ContinueLoop ; The plugins Enderal requires are always written back to plugins.txt as enabled, ; the remaining ones only when they were listed there before Local $bListed = IsRequiredPlugin($aFiles[$iFile][0]) Or FindListedPlugin($aListed, $aFiles[$iFile][0]) >= 0 AddPluginRow($aList, $iCount, $aFiles, $iFile, True, True, $bListed) Next ; Then the plugins.txt entries in their current order For $i = 0 To UBound($aListed) - 1 $iFile = FindPluginFile($aFiles, $aListed[$i][0]) If $iFile < 0 Then ContinueLoop ; entry without a file - drop it If FindPluginRow($aList, $iCount, $aFiles[$iFile][0]) >= 0 Then ContinueLoop AddPluginRow($aList, $iCount, $aFiles, $iFile, $aListed[$i][1], False, True) Next ; Finally the plugins that are not in plugins.txt yet, masters first For $iClass = $PLUGIN_CLASS_MASTER To $PLUGIN_CLASS_NORMAL For $i = 0 To $iFileCount - 1 If $aFiles[$i][1] <> $iClass Then ContinueLoop If FindPluginRow($aList, $iCount, $aFiles[$i][0]) >= 0 Then ContinueLoop AddPluginRow($aList, $iCount, $aFiles, $i, False, False, False) Next Next ; Keep masters above regular plugins - the engine loads them that way regardless of plugins.txt Local $aSorted[$iFileCount + 1][6] Local $iSorted = 0 For $iRank = 0 To 2 For $i = 0 To $iCount - 1 Local $iRowRank = $aList[$i][2] ? 0 : ($aList[$i][3] = $PLUGIN_CLASS_MASTER ? 1 : 2) If $iRowRank <> $iRank Then ContinueLoop For $c = 0 To 5 $aSorted[$iSorted][$c] = $aList[$i][$c] Next $iSorted += 1 Next Next $aPluginList = $aSorted $iPluginListCount = $iSorted $bPluginListLoaded = True FillPluginListView() EndFunc Func CreatePluginStateImages() ; Check box images for the plugin list: the two extra dimmed ones mark rows that cannot be changed. ; Without visual styles the list keeps the check boxes the control provides itself. If Not _WinAPI_IsThemeActive() Then Return Local $hTheme = _WinAPI_OpenThemeData($hLvPlugins, "BUTTON") If $hTheme = 0 Then Return Local $hWndDC = _WinAPI_GetDC($hLvPlugins) Local $tSize = _WinAPI_GetThemePartSize($hTheme, $THEME_BP_CHECKBOX, $THEME_CB_UNCHECKED, $hWndDC, 0, $TS_TRUE) Local $iError = @error _WinAPI_ReleaseDC($hLvPlugins, $hWndDC) Local $iWidth = 16, $iHeight = 16 If Not $iError Then $iWidth = DllStructGetData($tSize, "X") $iHeight = DllStructGetData($tSize, "Y") EndIf If $iWidth >= 8 And $iHeight >= 8 Then Local $hBitmap = CreateTransparentBitmap($iWidth * 4, $iHeight) If $hBitmap <> 0 Then ; Draw the four states next to each other, in state image index order Local $aStates[4] = [$THEME_CB_UNCHECKED, $THEME_CB_CHECKED, _ $THEME_CB_UNCHECKED_DISABLED, $THEME_CB_CHECKED_DISABLED] Local $hMemDC = _WinAPI_CreateCompatibleDC(0) Local $hPrevBitmap = _WinAPI_SelectObject($hMemDC, $hBitmap) Local $tRect = DllStructCreate($tagRECT) DllStructSetData($tRect, "Top", 0) DllStructSetData($tRect, "Bottom", $iHeight) For $i = 0 To UBound($aStates) - 1 DllStructSetData($tRect, "Left", $i * $iWidth) DllStructSetData($tRect, "Right", ($i + 1) * $iWidth) _WinAPI_DrawThemeBackground($hTheme, $THEME_BP_CHECKBOX, $aStates[$i], $hMemDC, $tRect) Next _WinAPI_SelectObject($hMemDC, $hPrevBitmap) _WinAPI_DeleteDC($hMemDC) $hPluginStateImages = _GUIImageList_Create($iWidth, $iHeight, 5, 0, 4, 4) If $hPluginStateImages <> 0 Then _GUIImageList_Add($hPluginStateImages, $hBitmap) _GUICtrlListView_SetImageList($hLvPlugins, $hPluginStateImages, 2) ; 2 = LVSIL_STATE EndIf _WinAPI_DeleteObject($hBitmap) EndIf EndIf _WinAPI_CloseThemeData($hTheme) EndFunc Func CreateTransparentBitmap($iWidth, $iHeight) ; Top down 32 bit device independent bitmap, cleared to fully transparent Local $tBitmapInfo = DllStructCreate("dword Size;long Width;long Height;ushort Planes;ushort BitCount;" & _ "dword Compression;dword SizeImage;long XPelsPerMeter;long YPelsPerMeter;dword ClrUsed;dword ClrImportant") DllStructSetData($tBitmapInfo, "Size", DllStructGetSize($tBitmapInfo)) DllStructSetData($tBitmapInfo, "Width", $iWidth) DllStructSetData($tBitmapInfo, "Height", -$iHeight) DllStructSetData($tBitmapInfo, "Planes", 1) DllStructSetData($tBitmapInfo, "BitCount", 32) DllStructSetData($tBitmapInfo, "Compression", 0) ; BI_RGB Local $aCall = DllCall("gdi32.dll", "handle", "CreateDIBSection", "handle", 0, "struct*", $tBitmapInfo, _ "uint", 0, "ptr*", 0, "handle", 0, "dword", 0) If @error Or Not IsArray($aCall) Then Return 0 Return $aCall[0] EndFunc Func IsPluginRowFixed($iIndex) ; True when the row cannot be toggled: plugins the game loads itself, or MO2 owning the load order If $bPluginListReadOnly Then Return True Return $aPluginList[$iIndex][2] EndFunc Func SetPluginRowState($iIndex) If $hPluginStateImages = 0 Then ; No dimmed images available - fall back to the plain check box _GUICtrlListView_SetItemChecked($hLvPlugins, $iIndex, $aPluginList[$iIndex][1]) Return EndIf Local $iState If IsPluginRowFixed($iIndex) Then $iState = $aPluginList[$iIndex][1] ? $PLUGIN_STATE_CHECKED_DIMMED : $PLUGIN_STATE_UNCHECKED_DIMMED Else $iState = $aPluginList[$iIndex][1] ? $PLUGIN_STATE_CHECKED : $PLUGIN_STATE_UNCHECKED EndIf _GUICtrlListView_SetItemState($hLvPlugins, $iIndex, BitShift($iState, -12), $LVIS_STATEIMAGEMASK) EndFunc Func FillPluginListView() $bPluginListUpdating = True _GUICtrlListView_BeginUpdate($hLvPlugins) _GUICtrlListView_DeleteAllItems($hLvPlugins) For $i = 0 To $iPluginListCount - 1 _GUICtrlListView_AddItem($hLvPlugins, $aPluginList[$i][0]) _GUICtrlListView_AddSubItem($hLvPlugins, $i, $aPluginList[$i][4], 1) SetPluginRowState($i) Next _GUICtrlListView_EndUpdate($hLvPlugins) $bPluginListUpdating = False UpdatePluginColumnWidths() UpdatePluginButtons() EndFunc Func UpdatePluginColumnWidths() ; Fit both columns into the visible area, with or without a scroll bar Local $tRect = _WinAPI_GetClientRect($hLvPlugins) If @error Then Return Local $iClientWidth = DllStructGetData($tRect, "Right") - DllStructGetData($tRect, "Left") Local $iTypeWidth = 120 If $iClientWidth < $iTypeWidth * 2 Then Return _GUICtrlListView_SetColumnWidth($hLvPlugins, 1, $iTypeWidth) _GUICtrlListView_SetColumnWidth($hLvPlugins, 0, $iClientWidth - $iTypeWidth) EndFunc Func UpdatePluginRow($iIndex) $bPluginListUpdating = True _GUICtrlListView_SetItemText($hLvPlugins, $iIndex, $aPluginList[$iIndex][0], 0) _GUICtrlListView_SetItemText($hLvPlugins, $iIndex, $aPluginList[$iIndex][4], 1) SetPluginRowState($iIndex) $bPluginListUpdating = False EndFunc Func GetSelectedPluginIndex() If $hLvPlugins = 0 Then Return -1 Local $aIndices = _GUICtrlListView_GetSelectedIndices($hLvPlugins, True) If UBound($aIndices) < 2 Or $aIndices[0] < 1 Then Return -1 Return $aIndices[1] EndFunc Func CanMovePlugin($iIndex, $iTarget) If $bPluginListReadOnly Then Return False If $iIndex < 0 Or $iIndex >= $iPluginListCount Then Return False If $iTarget < 0 Or $iTarget >= $iPluginListCount Then Return False ; Plugins loaded by the game itself keep their position If $aPluginList[$iIndex][2] Or $aPluginList[$iTarget][2] Then Return False ; Masters cannot be mixed with regular plugins If $aPluginList[$iIndex][3] <> $aPluginList[$iTarget][3] Then Return False Return True EndFunc Func MoveSelectedPlugin($iDirection) Local $iIndex = GetSelectedPluginIndex() Local $iTarget = $iIndex + $iDirection If Not CanMovePlugin($iIndex, $iTarget) Then Return For $c = 0 To 5 Local $swap = $aPluginList[$iIndex][$c] $aPluginList[$iIndex][$c] = $aPluginList[$iTarget][$c] $aPluginList[$iTarget][$c] = $swap Next UpdatePluginRow($iIndex) UpdatePluginRow($iTarget) $bPluginListUpdating = True _GUICtrlListView_SetItemSelected($hLvPlugins, $iIndex, False) _GUICtrlListView_SetItemSelected($hLvPlugins, $iTarget, True, True) $bPluginListUpdating = False _GUICtrlListView_EnsureVisible($hLvPlugins, $iTarget) $bPluginListDirty = True UpdatePluginButtons() EndFunc Func SetAllPluginsEnabled($bEnabled) If $bPluginListReadOnly Then Return For $i = 0 To $iPluginListCount - 1 If $aPluginList[$i][2] Then ContinueLoop ; locked plugins stay enabled If $aPluginList[$i][1] = $bEnabled Then ContinueLoop $aPluginList[$i][1] = $bEnabled UpdatePluginRow($i) $bPluginListDirty = True Next UpdatePluginButtons() EndFunc Func UpdatePluginButtons() Local $iIndex = GetSelectedPluginIndex() GUICtrlSetState($btnPluginUp, CanMovePlugin($iIndex, $iIndex - 1) ? $GUI_ENABLE : $GUI_DISABLE) GUICtrlSetState($btnPluginDown, CanMovePlugin($iIndex, $iIndex + 1) ? $GUI_ENABLE : $GUI_DISABLE) Local $iEnabled = 0 Local $iChangeable = 0 For $i = 0 To $iPluginListCount - 1 If $aPluginList[$i][1] Then $iEnabled += 1 If Not IsPluginRowFixed($i) Then $iChangeable += 1 Next GUICtrlSetData($lblPluginsCount, $iEnabled & " of " & $iPluginListCount & " enabled") GUICtrlSetState($btnPluginsEnableAll, $iChangeable > 0 ? $GUI_ENABLE : $GUI_DISABLE) GUICtrlSetState($btnPluginsDisableAll, $iChangeable > 0 ? $GUI_ENABLE : $GUI_DISABLE) EndFunc Func SavePluginsFile() ; Under MO2 the load order is owned by the active profile If $bPluginListReadOnly Then Return If Not $bPluginListLoaded Or Not $bPluginListDirty Or $iPluginListCount = 0 Then Return Local $content = "" For $i = 0 To $iPluginListCount - 1 ; Plugins the game loads on its own are only written back when they were listed before If $aPluginList[$i][2] And Not $aPluginList[$i][5] Then ContinueLoop Local $bEnabled = $aPluginList[$i][2] ? True : $aPluginList[$i][1] $content &= ($bEnabled ? "*" : "") & $aPluginList[$i][0] & @CRLF Next If Not DirExists($PLUGINS_DIR) Then DirCreate($PLUGINS_DIR) Local $hFile = FileOpen($PLUGINS_FILE, BitOR($FO_OVERWRITE, $FO_UTF8_NOBOM)) If $hFile = -1 Then MsgBox(16, "Error", "Unable to write the plugin list:" & @CRLF & $PLUGINS_FILE) Return EndIf FileWrite($hFile, $content) FileClose($hFile) $bPluginListDirty = False EndFunc ; ============================================ ; Main Loop ; ============================================ Func MainLoop() While 1 Local $msg = GUIGetMsg(1) Local $msgID = $msg[0] Local $msgHwnd = $msg[1] Switch $msgID Case $GUI_EVENT_CLOSE If $msgHwnd = $hMainGUI Then Exit ElseIf $msgHwnd = $hSettingsGUI Then HideSettings() EndIf Case $btnClose, $lblCloseShadow Exit Case $btnMinimize, $lblMinimizeShadow GUISetState(@SW_MINIMIZE, $hMainGUI) Case $btnExit Exit Case $btnPlay LaunchGame() Case $btnSettings ShowSettings() Case $lblCredits, $lblCreditsShadow ShellExecute("https://mod.pub/enderal-se/38/docs/5-credits") Case $lblWiki, $lblWikiShadow ShellExecute("https://wiki.en.sureai.net/Enderal") Case $lblSupport, $lblSupportShadow ShellExecute("https://go.eddoursul.win/enderal-se-launcher-support") Case $lblChangelog, $lblChangelogShadow ShellExecute("https://mod.pub/enderal-se/38/changelogs") Case $btnSave SaveSettings() HideSettings() Case $btnCancel HideSettings() Case $btnLow ApplyDetailPreset("Low") Case $btnMedium ApplyDetailPreset("Medium") Case $btnHigh ApplyDetailPreset("High") Case $btnUltra ApplyDetailPreset("Ultra") Case $chkSSEDisplayTweaks RefreshResolution() Case $sldFramerateLimit GUICtrlSetData($lblFramerateLimitValue, GUICtrlRead($sldFramerateLimit)) Case $btnOpenEnderalIni RunWithAssociation($ENDERAL_INI) Case $btnOpenEnderalPrefsIni RunWithAssociation($ENDERAL_PREFS_INI) Case $btnOpenCustomIni OpenCustomIni() Case $btnOpenModIni OpenModIni() Case $btnOpenSavegames OpenSavegamesDirectory() Case $btnPluginUp MoveSelectedPlugin(-1) Case $btnPluginDown MoveSelectedPlugin(1) Case $btnPluginsEnableAll SetAllPluginsEnabled(True) Case $btnPluginsDisableAll SetAllPluginsEnabled(False) EndSwitch WEnd EndFunc ; ============================================ ; Settings Show/Hide ; ============================================ Func ShowSettings() ; Load settings values LoadSettings() ; Select first tab GUICtrlSetData($tabSettings, 0) ; Show settings window GUISetState(@SW_SHOW, $hSettingsGUI) EndFunc Func HideSettings() ; Drop unsaved plugin changes so the list is read again the next time $bPluginListDirty = False GUISetState(@SW_HIDE, $hSettingsGUI) EndFunc ; ============================================ ; Settings Load/Save ; ============================================ Func RefreshResolution() ; Read base resolution from Skyrim prefs Local $width = IniRead($ENDERAL_PREFS_INI, "Display", "iSize W", @DesktopWidth) Local $height = IniRead($ENDERAL_PREFS_INI, "Display", "iSize H", @DesktopHeight) Local $resolution = $width & "x" & $height ; If SSE Display Tweaks checkbox is checked, read from its INI Local $sseDisplayTweaksIni = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.ini" If BitAND(GUICtrlRead($chkSSEDisplayTweaks), $GUI_CHECKED) And FileExists($sseDisplayTweaksIni) Then Local $sseRes = IniRead($sseDisplayTweaksIni, "Render", "Resolution", "") If $sseRes <> "" Then $resolution = $sseRes EndIf ; Load and enable framerate limit Local $fpsLimit = IniRead($sseDisplayTweaksIni, "Render", "FramerateLimit", "60") If Number($fpsLimit) < 30 Then $fpsLimit = 30 If Number($fpsLimit) > @DesktopRefresh Then $fpsLimit = @DesktopRefresh GUICtrlSetData($sldFramerateLimit, $fpsLimit) GUICtrlSetData($lblFramerateLimitValue, $fpsLimit) GUICtrlSetState($sldFramerateLimit, $GUI_ENABLE) GUICtrlSetState($lblFramerateLimit, $GUI_ENABLE) GUICtrlSetState($lblFramerateLimitValue, $GUI_ENABLE) Else ; Disable framerate limit slider GUICtrlSetData($sldFramerateLimit, 60) GUICtrlSetData($lblFramerateLimitValue, "60") GUICtrlSetState($sldFramerateLimit, $GUI_DISABLE) GUICtrlSetState($lblFramerateLimit, $GUI_DISABLE) GUICtrlSetState($lblFramerateLimitValue, $GUI_DISABLE) EndIf GUICtrlSetData($cmbResolution, $resolution) EndFunc Func LoadSettings() ; SSE Display Tweaks paths (used in multiple places) Local $dllPath = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.dll" Local $bakPath = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.dll.bak" Local $sseDisplayTweaksIni = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.ini" ; Load Display settings Local $width = IniRead($ENDERAL_PREFS_INI, "Display", "iSize W", @DesktopWidth) Local $height = IniRead($ENDERAL_PREFS_INI, "Display", "iSize H", @DesktopHeight) Local $resolution = $width & "x" & $height ; If SSE Display Tweaks is enabled, read resolution from its INI If FileExists($dllPath) And FileExists($sseDisplayTweaksIni) Then Local $sseRes = IniRead($sseDisplayTweaksIni, "Render", "Resolution", "") If $sseRes <> "" Then $resolution = $sseRes EndIf EndIf GUICtrlSetData($cmbResolution, $resolution) ; Load Antialiasing Local $taa = IniRead($ENDERAL_PREFS_INI, "Display", "bUseTAA", "0") Local $fxaa = IniRead($ENDERAL_PREFS_INI, "Display", "bFXAAEnabled", "0") If $taa = "1" Then GUICtrlSetData($cmbAntialiasing, $AA_TAA) ElseIf $fxaa = "1" Then GUICtrlSetData($cmbAntialiasing, $AA_FXAA) Else GUICtrlSetData($cmbAntialiasing, $AA_OFF) EndIf ; Load Windowed Mode Local $fullscreen = IniRead($ENDERAL_PREFS_INI, "Display", "bFull Screen", "1") SetCheckboxState($chkWindowed, $fullscreen, "0") ; Load Rendering settings If FileExists($dllPath) Then GUICtrlSetData($grpSSEDisplayTweaks, "SSE Display Tweaks") GUICtrlSetState($chkSSEDisplayTweaks, $GUI_CHECKED + $GUI_ENABLE) ; Load framerate limit from INI Local $fpsLimit = IniRead($sseDisplayTweaksIni, "Render", "FramerateLimit", "60") If Number($fpsLimit) < 30 Then $fpsLimit = 30 If Number($fpsLimit) > @DesktopRefresh Then $fpsLimit = @DesktopRefresh GUICtrlSetData($sldFramerateLimit, $fpsLimit) GUICtrlSetData($lblFramerateLimitValue, $fpsLimit) GUICtrlSetState($sldFramerateLimit, $GUI_ENABLE) GUICtrlSetState($lblFramerateLimit, $GUI_ENABLE) GUICtrlSetState($lblFramerateLimitValue, $GUI_ENABLE) ElseIf FileExists($bakPath) Then GUICtrlSetData($grpSSEDisplayTweaks, "SSE Display Tweaks") GUICtrlSetState($chkSSEDisplayTweaks, $GUI_UNCHECKED + $GUI_ENABLE) GUICtrlSetData($sldFramerateLimit, 60) GUICtrlSetData($lblFramerateLimitValue, "60") GUICtrlSetState($sldFramerateLimit, $GUI_DISABLE) GUICtrlSetState($lblFramerateLimit, $GUI_DISABLE) GUICtrlSetState($lblFramerateLimitValue, $GUI_DISABLE) Else GUICtrlSetData($grpSSEDisplayTweaks, "SSE Display Tweaks (not installed)") GUICtrlSetState($chkSSEDisplayTweaks, $GUI_UNCHECKED + $GUI_DISABLE) GUICtrlSetData($sldFramerateLimit, 60) GUICtrlSetData($lblFramerateLimitValue, "60") GUICtrlSetState($sldFramerateLimit, $GUI_DISABLE) GUICtrlSetState($lblFramerateLimit, $GUI_DISABLE) GUICtrlSetState($lblFramerateLimitValue, $GUI_DISABLE) EndIf SetCheckboxState($chk64bitRT, IniRead($ENDERAL_PREFS_INI, "Display", "bUse64bitsHDRRenderTarget", "0")) SetCheckboxState($chkSSAO, IniRead($ENDERAL_PREFS_INI, "Display", "bSAOEnable", "0")) SetCheckboxState($chkVolumetricLighting, IniRead($ENDERAL_PREFS_INI, "Display", "bVolumetricLightingEnable", "0")) Local $ssr = IniReadWithCustom("Display", "fWaterSSRIntensity", "0.0001") SetCheckboxState($chkSSR, Number($ssr) > 1 ? "1" : "0") ; Load Controls settings SetCheckboxState($chkController, IniRead($ENDERAL_PREFS_INI, "MAIN", "bGamepadEnable", "0")) SetCheckboxState($chkVibration, IniRead($ENDERAL_PREFS_INI, "Controls", "bGamePadRumble", "0")) ; Update INI Files group label based on SkyrimRedirector status Local $redirectorDll = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SkyrimRedirector.dll" If FileExists($redirectorDll) Then GUICtrlSetData($grpIniFiles, "INI Files (SkyrimRedirector is active)") Else GUICtrlSetData($grpIniFiles, "INI Files (SkyrimRedirector is not installed)") EndIf ; Load Start Steam checkbox (Steam version only) If $bIsSteamVersion Then Local $startSteam = RegRead($REG_KEY, $REG_START_STEAM) If @error Then $startSteam = 1 SetCheckboxState($chkStartSteam, $startSteam) EndIf ; Load the plugin list LoadPluginList() EndFunc Func SaveSettings() ; Save Resolution Local $resolution = GUICtrlRead($cmbResolution) Local $resParts = StringSplit($resolution, "x") If $resParts[0] = 2 Then IniWrite($ENDERAL_PREFS_INI, "Display", "iSize W", $resParts[1]) IniWrite($ENDERAL_PREFS_INI, "Display", "iSize H", $resParts[2]) ; Sync resolution to SSEDisplayTweaks.ini if present Local $sseDisplayTweaksIni = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.ini" If FileExists($sseDisplayTweaksIni) Then Local $currentRes = IniRead($sseDisplayTweaksIni, "Render", "Resolution", "") If $currentRes <> "" Then IniWrite($sseDisplayTweaksIni, "Render", "Resolution", $resolution) EndIf EndIf EndIf ; Save Antialiasing Local $aa = GUICtrlRead($cmbAntialiasing) Switch $aa Case $AA_TAA IniWrite($ENDERAL_PREFS_INI, "Display", "bUseTAA", "1") IniWrite($ENDERAL_PREFS_INI, "Display", "bFXAAEnabled", "0") Case $AA_FXAA IniWrite($ENDERAL_PREFS_INI, "Display", "bUseTAA", "0") IniWrite($ENDERAL_PREFS_INI, "Display", "bFXAAEnabled", "1") Case $AA_OFF IniWrite($ENDERAL_PREFS_INI, "Display", "bUseTAA", "0") IniWrite($ENDERAL_PREFS_INI, "Display", "bFXAAEnabled", "0") EndSwitch ; Save Windowed Mode (inverted: checked = "0" for fullscreen) WriteCheckboxToIni($chkWindowed, $ENDERAL_PREFS_INI, "Display", "bFull Screen", "0", "1") ; Save SSE Display Tweaks (file-based toggle) Local $dllPath = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.dll" Local $bakPath = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.dll.bak" Local $sseDisplayTweaksIni = $LAUNCHER_DIR & "\Data\SKSE\Plugins\SSEDisplayTweaks.ini" If BitAND(GUICtrlRead($chkSSEDisplayTweaks), $GUI_CHECKED) Then If FileExists($bakPath) And Not FileExists($dllPath) Then FileMove($bakPath, $dllPath) ; Save framerate limit If FileExists($sseDisplayTweaksIni) Then IniWrite($sseDisplayTweaksIni, "Render", "FramerateLimit", GUICtrlRead($sldFramerateLimit)) EndIf Else If FileExists($dllPath) Then FileMove($dllPath, $bakPath, 1) EndIf ; Save Rendering settings WriteCheckboxToIni($chk64bitRT, $ENDERAL_PREFS_INI, "Display", "bUse64bitsHDRRenderTarget") WriteCheckboxToIni($chkSSAO, $ENDERAL_PREFS_INI, "Display", "bSAOEnable") WriteCheckboxToIni($chkVolumetricLighting, $ENDERAL_PREFS_INI, "Display", "bVolumetricLightingEnable") WriteCheckboxWithCustom($chkSSR, "Display", "fWaterSSRIntensity", "1.3", "0.0001") ; Save Controls settings WriteCheckboxToIni($chkController, $ENDERAL_PREFS_INI, "MAIN", "bGamepadEnable") WriteCheckboxToIni($chkVibration, $ENDERAL_PREFS_INI, "Controls", "bGamePadRumble") ; Save Start Steam checkbox (Steam version only) If $bIsSteamVersion Then Local $startSteam = (GUICtrlRead($chkStartSteam) = $GUI_CHECKED) ? 1 : 0 RegWrite($REG_KEY, $REG_START_STEAM, "REG_DWORD", $startSteam) EndIf ; Save the plugin list SavePluginsFile() EndFunc Func ApplyDetailPreset($presetName) Local $presetFile = $LAUNCHER_DIR & "\" & $presetName & ".ini" If Not FileExists($presetFile) Then MsgBox(16, "Error", "Preset file not found: " & $presetFile) Return EndIf Local $sections = IniReadSectionNames($presetFile) If @error Then MsgBox(16, "Error", "Could not read preset file " & $presetFile) Return EndIf For $i = 1 To $sections[0] Local $sectionData = IniReadSection($presetFile, $sections[$i]) If Not @error Then For $j = 1 To $sectionData[0][0] IniWrite($ENDERAL_PREFS_INI, $sections[$i], $sectionData[$j][0], $sectionData[$j][1]) Next EndIf Next MsgBox(64, "Settings Updated", "Video settings have been updated.") LoadSettings() EndFunc ; ============================================ ; Game Launch ; ============================================ Func LaunchGame() If Not FileExists($LAUNCHER_DIR & "\skse64_loader.exe") Then MsgBox(16, "Error", "Unable to find skse64_loader.exe! SKSE is not installed.") Return EndIf ; Hide main buttons GUICtrlSetState($btnPlay, $GUI_HIDE) GUICtrlSetState($btnSettings, $GUI_HIDE) GUICtrlSetState($btnExit, $GUI_HIDE) ; Create launching modal (aligned with main buttons) Local $modalWidth = 200 Local $modalHeight = 70 Local $modalX = $LAUNCHER_WIDTH - $modalWidth - 60 Local $modalY = ($LAUNCHER_HEIGHT - $modalHeight) / 2 Local $hLaunchingGUI = GUICreate("", $modalWidth, $modalHeight, $modalX, $modalY, $WS_POPUP, $WS_EX_MDICHILD, $hMainGUI) GUISetBkColor(0x1A1A1A, $hLaunchingGUI) GUICtrlCreateLabel("Launching...", 0, 20, $modalWidth, 35, $SS_CENTER) GUICtrlSetFont(-1, 14, 400, 0, "Tahoma") GUICtrlSetColor(-1, 0xFFFFFF) GUICtrlSetBkColor(-1, $GUI_BKCOLOR_TRANSPARENT) GUISetState(@SW_SHOW, $hLaunchingGUI) ; Set DPI awareness for SkyrimSE.exe SetDPIAwareness() ; Launch SKSE Run($LAUNCHER_DIR & "\skse64_loader.exe", $LAUNCHER_DIR) ; Wait for SkyrimSE.exe window to appear (GOG or Steam version) While 1 If WinExists("[CLASS:Skyrim Special Edition GOG]") Or WinExists("[CLASS:Skyrim Special Edition]") Then ExitLoop EndIf ; Also check if the process is no longer running (user closed it quickly) If Not ProcessExists("SkyrimSE.exe") And Not ProcessExists("skse64_loader.exe") Then ; Process ended before window appeared, close modal and restore buttons GUIDelete($hLaunchingGUI) GUICtrlSetState($btnPlay, $GUI_SHOW) GUICtrlSetState($btnSettings, $GUI_SHOW) GUICtrlSetState($btnExit, $GUI_SHOW) Return EndIf Sleep(100) WEnd ; SkyrimSE.exe window exists, exit launcher Exit EndFunc ; ============================================ ; Get Mod Organizer 2 Directory ; ============================================ Func GetMO2Directory() Local $parentPID = _WinAPI_GetParentProcess() If @error Or $parentPID = 0 Then Return "" Local $parentPath = _WinAPI_GetProcessFileName($parentPID) If @error Or $parentPath = "" Then Return "" ; Check if parent is ModOrganizer.exe If StringInStr($parentPath, "ModOrganizer.exe") Then Return StringRegExpReplace($parentPath, "\\[^\\]+$", "") EndIf Return "" EndFunc ; ============================================ ; Open Savegames Directory ; ============================================ Func OpenSavegamesDirectory() Local $savePath = IniReadWithCustom("General", "sLocalSavePath", "Saves\") Local $skyrimDocsDir = GetDocumentsPath() & "\My Games\" & GetBaseDirectoryName() Local $fullSavePath = $skyrimDocsDir & "\" & $savePath If Not DirExists($fullSavePath) Then MsgBox(16, "Error", "Savegames directory not found: " & $fullSavePath) Return EndIf ; Check if running under Mod Organizer (parent process is ModOrganizer.exe) If $MO2_DIR <> "" Then Local $explorerPPPath = $MO2_DIR & "\explorer++\Explorer++.exe" If FileExists($explorerPPPath) Then Run('"' & $explorerPPPath & '" "' & $fullSavePath & '"') Return EndIf ; Explorer++ not found, show path in message box MsgBox(64, "Saved Games Location", "Your saved games are located at:" & @CRLF & @CRLF & $fullSavePath) Else ; Not running under MO2, open normally ShellExecute($fullSavePath) EndIf EndFunc ; ============================================ ; Open Custom INI File ; ============================================ Func OpenCustomIni() Local $customIniPath = GetCustomIniPath() If FileExists($customIniPath) Then RunWithAssociation($customIniPath) EndIf EndFunc ; ============================================ ; Open Mod INI File ; ============================================ Func OpenModIni() Local $modIniPath = GetDataIniPath() If FileExists($modIniPath) Then RunWithAssociation($modIniPath) EndIf EndFunc