diff --git a/.gitignore b/.gitignore index d0a7709c..ff617eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -398,6 +398,7 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +Game.Assets/hl2/steam.inf Game.Assets/hl2/maps/gm_flatgrass.bsp Game.Assets/hl2/maps/gm_bigcity.bsp Game.Assets/hl2/maps/gm_flatgrass_classic.bsp diff --git a/Game.Assets/Assets.cs b/Game.Assets/Assets.cs new file mode 100644 index 00000000..cc2292d1 --- /dev/null +++ b/Game.Assets/Assets.cs @@ -0,0 +1,477 @@ +using System.Runtime.InteropServices; +using Source.GUI.Controls; +using Source.Common.Commands; +using Source.Common.Formats.Keyvalues; +using System.Diagnostics; +using Source; + +namespace Game.Assets; + +static class AssetUtils +{ + public record AssetMapping(string LocalPath, string RemotePath); + private const int GModAppID = 4000; + private const string GModLocalPath = "steamapps/common/GarrysMod"; + + public static void CheckRequired() { + string? root = Path.Combine(FindProjectRoot(), "Game.Assets"); + + if (File.Exists(Path.Combine(root, "hl2", "garrysmod_dir.vpk"))) + return; + + bool result = Singleton()("Source.NET", "Missing required content, should we automatically link it?", true); + + if (result) + LinkAllAssets(root, true); + } + + public static List GetRequiredAssets() { + List list = [ + new("hl2/steam.inf", "garrysmod/steam.inf") + ]; + + string[] specificGmodVpks = ["dir", "000", "001", "002"]; + foreach (var suffix in specificGmodVpks) + list.Add(new($"hl2/garrysmod_{suffix}.vpk", $"garrysmod/garrysmod_{suffix}.vpk")); + + string[] specificHl2Vpks = ["dir", "000", "001", "002", "003", "004", "005", "006"]; + foreach (var suffix in specificHl2Vpks) + list.Add(new($"hl2/content_hl2_{suffix}.vpk", $"sourceengine/content_hl2_{suffix}.vpk")); + + string[] misc = ["dir", "000", "001", "002", "003"]; + foreach (var suffix in misc) + list.Add(new($"hl2/hl2_misc_{suffix}.vpk", $"sourceengine/hl2_misc_{suffix}.vpk")); + + string[] sound = ["dir", "000", "001", "002"]; + foreach (var suffix in sound) + list.Add(new($"hl2/hl2_sound_misc_{suffix}.vpk", $"sourceengine/hl2_sound_misc_{suffix}.vpk")); + + string[] tex = ["dir", "000", "001", "002", "003", "004", "005", "006", "007", "008", "009", "010"]; + foreach (var suffix in tex) + list.Add(new($"hl2/hl2_textures_{suffix}.vpk", $"sourceengine/hl2_textures_{suffix}.vpk")); + + return list; + } + + public static string? FindGarrysModPath() { + string? steamPath = null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + steamPath = Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Valve\Steam", "InstallPath", null) as string; + if (string.IsNullOrEmpty(steamPath)) { + steamPath = Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432NODE\Valve\Steam", "InstallPath", null) as string; + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string[] possiblePaths = [ + Path.Combine(home, ".local", "share", "Steam"), + Path.Combine(home, ".steam", "steam") + ]; + + foreach (var p in possiblePaths) { + if (Directory.Exists(p)) { + steamPath = p; + break; + } + } + } + + if (string.IsNullOrEmpty(steamPath)) + return null; + + string libraryFolders = Path.Combine(steamPath, "steamapps", "libraryfolders.vdf"); + if (!File.Exists(libraryFolders)) + return null; + + try { + string content = File.ReadAllText(libraryFolders); + return ParseLibraryFoldersForApp(content, GModAppID); + } + catch { + return null; + } + } + + private static string? ParseLibraryFoldersForApp(string vdfContent, int appId) { + int index = 0; + while (true) { + int pathKey = vdfContent.IndexOf("\"path\"", index, StringComparison.OrdinalIgnoreCase); + if (pathKey == -1) break; + + int pathStart = vdfContent.IndexOf('"', pathKey + 6); + int pathEnd = vdfContent.IndexOf('"', pathStart + 1); + if (pathStart == -1 || pathEnd == -1) break; + + string path = vdfContent.Substring(pathStart + 1, pathEnd - pathStart - 1).Replace("\\\\", "\\"); + + int appsKey = vdfContent.IndexOf("\"apps\"", pathEnd, StringComparison.OrdinalIgnoreCase); + if (appsKey != -1) { + int appsBlockStart = vdfContent.IndexOf('{', appsKey); + string appToken = $"\"{appId}\""; + int appIndex = vdfContent.IndexOf(appToken, appsBlockStart, StringComparison.OrdinalIgnoreCase); + + int nextPath = vdfContent.IndexOf("\"path\"", appsBlockStart, StringComparison.OrdinalIgnoreCase); + + if (appIndex != -1 && (nextPath == -1 || appIndex < nextPath)) { + return Path.Combine(path, GModLocalPath); + } + } + + index = pathEnd; + } + + return null; + } + + public static bool IsAssetLinked(string localRelativePath, string projectRoot) { + string fullPath = Path.Combine(projectRoot, localRelativePath); + return File.Exists(fullPath); + } + + public static bool UnlinkAsset(string localRelativePath, string projectRoot) { + try { + string fullLocalPath = Path.Combine(projectRoot, localRelativePath); + if (File.Exists(fullLocalPath)) { + File.Delete(fullLocalPath); + return true; + } + return false; + } + catch { + return false; + } + } + + public static bool BatchLinkAssets(List<(string Local, string Remote)> assets) { + foreach (var asset in assets) { + string? dir = Path.GetDirectoryName(asset.Local); + if (dir != null && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + if (File.Exists(asset.Local)) + File.Delete(asset.Local); + } + + try { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + string batchFile = Path.Combine(Path.GetTempPath(), $"sdn_link_{Guid.NewGuid()}.bat"); + using (var writer = File.CreateText(batchFile)) { + writer.WriteLine("@echo off"); + foreach (var asset in assets) { + writer.WriteLine($"mklink \"{asset.Local}\" \"{asset.Remote}\""); + } + } + + var startInfo = new ProcessStartInfo { + FileName = "cmd.exe", + Arguments = $"/c \"{batchFile}\"", + UseShellExecute = true, + Verb = "runas", + WindowStyle = ProcessWindowStyle.Hidden + }; + var proc = Process.Start(startInfo); + if (proc == null) return false; + proc.WaitForExit(); + + if (File.Exists(batchFile)) + File.Delete(batchFile); + } + else { + foreach (var asset in assets) { + var startInfo = new ProcessStartInfo { + FileName = "ln", + Arguments = $"-s \"{asset.Remote}\" \"{asset.Local}\"", + UseShellExecute = false, + CreateNoWindow = true + }; + var proc = Process.Start(startInfo); + proc?.WaitForExit(); + } + } + return true; + } + catch (Exception e) { + Console.WriteLine($"BatchLinkAssets failed: {e.Message}"); + return false; + } + } + + public static string FindProjectRoot() { + string? currentDir = AppDomain.CurrentDomain.BaseDirectory; + while (!string.IsNullOrEmpty(currentDir)) { + if (File.Exists(Path.Combine(currentDir, "Source.NET.sln"))) + return currentDir; + + currentDir = Directory.GetParent(currentDir)?.FullName; + } + return AppDomain.CurrentDomain.BaseDirectory!; + } + + public static void LinkAllAssets(string projectRoot, bool requireRestart = false) { + string? gmodPath = FindGarrysModPath(); + if (gmodPath == null) + return; + + List<(string, string)> assets = []; + var required = GetRequiredAssets(); + + foreach (var asset in required) { + if (IsAssetLinked(asset.LocalPath, projectRoot)) + continue; + + string fullLocalPath = Path.Combine(projectRoot, asset.LocalPath); + string remotePath = Path.Combine(gmodPath, asset.RemotePath); + assets.Add((fullLocalPath, remotePath)); + } + + if (assets.Count > 0) { + if (BatchLinkAssets(assets)) { + if (requireRestart) { + List<(string, string)> outputAssets = []; + foreach (var (local, remote) in assets) { + var relative = Path.GetRelativePath(projectRoot, local); + var dest = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, relative); + outputAssets.Add((dest, remote)); + } + BatchLinkAssets(outputAssets); + } + } + } + } +} + +public class AssetLinker : Frame +{ + private ListPanel ListPanel; + private Button LinkButton; + private Button UnlinkAllButton; + private Button LinkSelectedButton; + private Button UnlinkSelectedButton; + private Button RefreshButton; + private Button CloseButton; + + private readonly List AssetsStringList; + private readonly string ProjectRoot; + + public AssetLinker() : base(null, "AssetLinker") { + ProjectRoot = AssetUtils.FindProjectRoot(); + if (string.IsNullOrEmpty(ProjectRoot) || !Directory.Exists(ProjectRoot)) + ProjectRoot = AppDomain.CurrentDomain.BaseDirectory!; + + ProjectRoot = Path.Combine(ProjectRoot, "Game.Assets"); + + SetTitle("Asset Linker", true); + SetSize(620, 400); + SetMoveable(true); + SetSizeable(true); + SetVisible(true); + + ListPanel = new ListPanel(this, "AssetList"); + ListPanel.AddColumnHeader(0, "local", "Local Path", 250, ListPanel.ColumnFlags.ResizeWithWindow); + ListPanel.AddColumnHeader(1, "remote", "Remote Target", 250, ListPanel.ColumnFlags.ResizeWithWindow); + ListPanel.AddColumnHeader(2, "status", "Status", 80, 0); + + LinkButton = new Button(this, "LinkButton", "Link All", this, "LinkAll"); + UnlinkAllButton = new Button(this, "UnlinkAllButton", "Unlink All", this, "UnlinkAll"); + LinkSelectedButton = new Button(this, "LinkSelectedButton", "Link Selected", this, "LinkSelected"); + UnlinkSelectedButton = new Button(this, "UnlinkSelectedButton", "Unlink Selected", this, "UnlinkSelected"); + RefreshButton = new Button(this, "RefreshButton", "Refresh", this, "Refresh"); + CloseButton = new Button(this, "CloseButton", "Close", this, "Close"); + + AssetsStringList = AssetUtils.GetRequiredAssets(); + + RefreshList(); + UpdateButtons(); + } + + public override void PerformLayout() { + base.PerformLayout(); + int wide = GetWide(); + int tall = GetTall(); + + ListPanel.SetBounds(10, 30, wide - 20, tall - 70); + + int buttonHeight = 24; + int buttonY = tall - 35; + + LinkSelectedButton.SetBounds(10, buttonY, 110, buttonHeight); + UnlinkSelectedButton.SetBounds(130, buttonY, 110, buttonHeight); + RefreshButton.SetBounds(250, buttonY, 80, buttonHeight); + + LinkButton.SetBounds(wide - 270, buttonY, 80, buttonHeight); + UnlinkAllButton.SetBounds(wide - 180, buttonY, 80, buttonHeight); + CloseButton.SetBounds(wide - 90, buttonY, 80, buttonHeight); + } + + public override void OnThink() { + base.OnThink(); + UpdateButtons(); + } + + private void UpdateButtons() { + bool anyMissing = false; + bool anyLinked = false; + + foreach (var asset in AssetsStringList) { + if (AssetUtils.IsAssetLinked(asset.LocalPath, ProjectRoot)) + anyLinked = true; + else + anyMissing = true; + } + + LinkButton.SetEnabled(anyMissing); + UnlinkAllButton.SetEnabled(anyLinked); + + bool canLinkSelected = false; + bool canUnlinkSelected = false; + + int selectedCount = ListPanel.GetSelectedItemsCount(); + if (selectedCount > 0) { + int selectedItem = ListPanel.GetSelectedItem(0); + KeyValues? data = ListPanel.GetItem(selectedItem); + if (data != null) { + string status = data.GetString("status", "").ToString(); + canLinkSelected = status != "Linked"; + canUnlinkSelected = status == "Linked"; + } + } + + LinkSelectedButton.SetEnabled(canLinkSelected); + UnlinkSelectedButton.SetEnabled(canUnlinkSelected); + } + + public override void OnCommand(ReadOnlySpan command) { + if (command.SequenceEqual("LinkAll")) + LinkAllAssets(); + else if (command.SequenceEqual("UnlinkAll")) + UnlinkAllAssets(); + else if (command.SequenceEqual("LinkSelected")) + LinkSelectedAsset(); + else if (command.SequenceEqual("UnlinkSelected")) + UnlinkSelectedAsset(); + else if (command.SequenceEqual("Refresh")) + RefreshList(); + else if (command.SequenceEqual("Close")) + Close(); + else + base.OnCommand(command); + } + + private void LinkSelectedAsset() { + if (ListPanel.GetSelectedItemsCount() == 0) + return; + + int selectedItem = ListPanel.GetSelectedItem(0); + if (selectedItem == -1) + return; + + KeyValues? data = ListPanel.GetItem(selectedItem); + if (data == null) + return; + + string localPath = data.GetString("local", "").ToString(); + string remotePath = data.GetString("remote", "").ToString(); + + if (string.IsNullOrEmpty(localPath) || string.IsNullOrEmpty(remotePath) || remotePath == "Not Found") + return; + + string relPath = Path.GetRelativePath(ProjectRoot, localPath); + + if (AssetUtils.IsAssetLinked(relPath, ProjectRoot)) + return; + + AssetUtils.BatchLinkAssets([(localPath, remotePath)]); + RefreshList(); + } + + private void UnlinkSelectedAsset() { + if (ListPanel.GetSelectedItemsCount() == 0) + return; + + int selectedItem = ListPanel.GetSelectedItem(0); + if (selectedItem == -1) + return; + + KeyValues? data = ListPanel.GetItem(selectedItem); + if (data == null) + return; + + string localPath = data.GetString("local", "").ToString(); + if (string.IsNullOrEmpty(localPath)) + return; + + string relPath = Path.GetRelativePath(ProjectRoot, localPath); + + if (!AssetUtils.IsAssetLinked(relPath, ProjectRoot)) + return; + + AssetUtils.UnlinkAsset(relPath, ProjectRoot); + RefreshList(); + } + + private void RefreshList() { + string? selectedPath = null; + if (ListPanel.GetSelectedItemsCount() > 0) { + int selectedItem = ListPanel.GetSelectedItem(0); + KeyValues? data = ListPanel.GetItem(selectedItem); + if (data != null) + selectedPath = data.GetString("local", "").ToString(); + } + + ListPanel.DeleteAllItems(); + + string? gmodPath = AssetUtils.FindGarrysModPath(); + + foreach (var asset in AssetsStringList) { + var kv = new KeyValues("item"); + string fullLocalPath = Path.Combine(ProjectRoot, asset.LocalPath); + kv.SetString("local", fullLocalPath); + kv.SetString("remote", gmodPath != null ? Path.Combine(gmodPath, asset.RemotePath) : "Not Found"); + + bool isLinked = AssetUtils.IsAssetLinked(asset.LocalPath, ProjectRoot); + kv.SetString("status", isLinked ? "Linked" : "Missing"); + + int itemID = ListPanel.AddItem(kv, 0, false, false); + + if (selectedPath != null && fullLocalPath == selectedPath) + ListPanel.AddSelectedItem(itemID); + } + + UpdateButtons(); + } + + private void LinkAllAssets() { + string? gmodPath = AssetUtils.FindGarrysModPath(); + if (gmodPath == null) + return; + + AssetUtils.LinkAllAssets(ProjectRoot); + RefreshList(); + } + + private void UnlinkAllAssets() { + foreach (var asset in AssetsStringList) { + if (!AssetUtils.IsAssetLinked(asset.LocalPath, ProjectRoot)) + continue; + + AssetUtils.UnlinkAsset(asset.LocalPath, ProjectRoot); + } + + RefreshList(); + } + + public static void CheckRequired() => AssetUtils.CheckRequired(); + + private static AssetLinker? Linker; + [ConCommand("sdn_assets")] + public static void sdn_assets() { + if (Linker != null) { + Linker.SetVisible(true); + Linker.MoveToFront(); + return; + } + + Linker = new AssetLinker(); + Linker.Activate(); + } +} diff --git a/Game.Assets/Game.Assets.csproj b/Game.Assets/Game.Assets.csproj new file mode 100644 index 00000000..d7a50cee --- /dev/null +++ b/Game.Assets/Game.Assets.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + AnyCPU + + + + + + + + + + + diff --git a/Game.Assets/autosymlink.ps1 b/Game.Assets/autosymlink.ps1 deleted file mode 100644 index 0d750d58..00000000 --- a/Game.Assets/autosymlink.ps1 +++ /dev/null @@ -1,412 +0,0 @@ -# This automatically finds where Garry's Mod is installed and symlinks the necessary files for Source.NET to run. - -# I want to credit this code down here, but I forgot who made the VDF parser, it's been a while -Enum State -{ - Start = 0 - Property = 1 - Object = 2 - Conditional = 3 - Finished = 4 - Closed = 5 -}; - -Class VdfDeserializer -{ - [PSCustomObject] Deserialize([string]$vdfContent) - { - if([string]::IsNullOrWhiteSpace($vdfContent)) { - throw 'Mandatory argument $vdfContent must be a non-empty, non-whitespace object of type [string]'; - } - - [System.IO.TextReader]$reader = [System.IO.StringReader]::new($vdfContent); - return $this.Deserialize($reader); - } - - [PSCustomObject] Deserialize([System.IO.TextReader]$txtReader) - { - if( !$txtReader ){ - throw 'Mandatory arguments $textReader missing.'; - } - - $vdfReader = [VdfTextReader]::new($txtReader); - $result = [PSCustomObject]@{ }; - - try - { - if (!$vdfReader.ReadToken()) - { - throw "Incomplete VDF data."; - } - - $prop = $this.ReadProperty($vdfReader); - Add-Member -InputObject $result -MemberType NoteProperty -Name $prop.Key -Value $prop.Value; - } - finally - { - if($vdfReader) - { - $vdfReader.Close(); - } - } - return $result; - } - - [hashtable] ReadProperty([VdfTextReader]$vdfReader) - { - $key=$vdfReader.Value; - - if (!$vdfReader.ReadToken()) - { - throw "Incomplete VDF data."; - } - - if ($vdfReader.CurrentState -eq [State]::Property) - { - $result = @{ - Key = $key - Value = $vdfReader.Value - } - } - else - { - $result = @{ - Key = $key - Value = $this.ReadObject($vdfReader); - } - } - return $result; - } - - [PSCustomObject] ReadObject([VdfTextReader]$vdfReader) - { - $result = [PSCustomObject]@{ }; - - if (!$vdfReader.ReadToken()) - { - throw "Incomplete VDF data."; - } - - while ( ($vdfReader.CurrentState -ne [State]::Object) -or ($vdfReader.Value -ne "}")) - { - [hashtable]$prop = $this.ReadProperty($vdfReader); - - Add-Member -InputObject $result -MemberType NoteProperty -Name $prop.Key -Value $prop.Value; - - if (!$vdfReader.ReadToken()) - { - throw "Incomplete VDF data."; - } - } - - return $result; - } -} - -Class VdfTextReader -{ - [string]$Value; - [State]$CurrentState; - - hidden [ValidateNotNull()][System.IO.TextReader]$_reader; - - hidden [ValidateNotNull()][char[]]$_charBuffer=; - hidden [ValidateNotNull()][char[]]$_tokenBuffer=; - - hidden [int32]$_charPos; - hidden [int32]$_charsLen; - hidden [int32]$_tokensize; - hidden [bool]$_isQuoted; - - VdfTextReader([System.IO.TextReader]$txtReader) - { - if( !$txtReader ){ - throw "Mandatory arguments `$textReader missing."; - } - - $this._reader = $txtReader; - - $this._charBuffer=[char[]]::new(1024); - $this._tokenBuffer=[char[]]::new(4096); - - $this._charPos=0; - $this._charsLen=0; - $this._tokensize=0; - $this._isQuoted=$false; - - $this.Value=""; - $this.CurrentState=[State]::Start; - } - - <# - .SYNOPSIS - Reads a single token. The value is stored in the $Value property - - .DESCRIPTION - Returns $true if a token was read, $false otherwise. - #> - [bool] ReadToken() - { - if (!$this.SeekToken()) - { - return $false; - } - - $this._tokenSize = 0; - - while($this.EnsureBuffer()) - { - [char]$curChar = $this._charBuffer[$this._charPos]; - - #No special treatment for escape characters - - #region Quote - if ($curChar -eq '"' -or (!$this._isQuoted -and [Char]::IsWhiteSpace($curChar))) - { - $this.Value = [string]::new($this._tokenBuffer, 0, $this._tokenSize); - $this.CurrentState = [State]::Property; - $this._charPos++; - return $true; - } - #endregion Quote - - #region Object Start/End - if (($curChar -eq '{') -or ($curChar -eq '}')) - { - if ($this._isQuoted) - { - $this._tokenBuffer[$this._tokenSize++] = $curChar; - $this._charPos++; - continue; - } - elseif ($this._tokenSize -ne 0) - { - $this.Value = [string]::new($this._tokenBuffer, 0, $this._tokenSize); - $this.CurrentState = [State]::Property; - return $true; - } - else - { - $this.Value = $curChar.ToString(); - $this.CurrentState = [State]::Object; - $this._charPos++; - return $true; - } - } - #endregion Object Start/End - - #region Long Token - $this._tokenBuffer[$this._tokenSize++] = $curChar; - $this._charPos++; - #endregion Long Token - } - - return $false; - } - - [void] Close() - { - $this.CurrentState = [State]::Closed; - } - - <# - .SYNOPSIS - Seeks the next token in the buffer. - - .DESCRIPTION - Returns $true if a token was found, $false otherwise. - #> - hidden [bool] SeekToken() - { - while($this.EnsureBuffer()) - { - # Skip Whitespace - if( [char]::IsWhiteSpace($this._charBuffer[$this._charPos]) ) - { - $this._charPos++; - continue; - } - - # Token - if ($this._charBuffer[$this._charPos] -eq '"') - { - $this._isQuoted = $true; - $this._charPos++; - return $true; - } - - # Comment - if ($this._charBuffer[$this._charPos] -eq '/') - { - $this.SeekNewLine(); - $this._charPos++; - continue; - } - - $this._isQuoted = $false; - return $true; - } - - return $false; - } - - <# - .SYNOPSIS - Seeks the next newline in the buffer. - - .DESCRIPTION - Returns $true if \n was found, $false otherwise. - #> - hidden [bool] SeekNewLine() - { - while ($this.EnsureBuffer()) - { - if ($this._charBuffer[++$this._charPos] == '\n') - { - return $true; - } - } - return $false; - } - - <# - .SYNOPSIS - Refills the buffer if we're at the end. - - .DESCRIPTION - Returns $false if the stream was empty, $true otherwise. - #> - hidden [bool]EnsureBuffer() - { - if($this._charPos -lt $this._charsLen -1) - { - return $true; - } - - [int32] $remainingChars = $this._charsLen - $this._charPos; - $this._charBuffer[0] = $this._charBuffer[($this._charsLen - 1) * $remainingChars]; #A bit of mathgic to improve performance by avoiding a conditional. - $this._charsLen = $this._reader.Read($this._charBuffer, $remainingChars, 1024 - $remainingChars) + $remainingChars; - $this._charPos = 0; - - return ($this._charsLen -ne 0); - } -} - -$steamInstallPath = Get-ItemProperty -Path "HKLM:\SOFTWARE\Valve\Steam" -Name "InstallPath" -ErrorAction SilentlyContinue - -if (-not $steamInstallPath) { - $steamInstallPath = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432NODE\Valve\Steam" -Name "InstallPath" -ErrorAction SilentlyContinue -} - -$gmodLocalPath = "\steamapps\common\GarrysMod" -$gmodAppID = 4000; - -function Link-SDN-Asset { - param ( - [string]$SDN_ABS, - [string]$GMOD_ABS, - [string]$SDN_LOCAL, - [string]$GMOD_LOCAL - ) - - $linkPath = Join-Path $SDN_ABS $SDN_LOCAL - $targetPath = Join-Path $GMOD_ABS $GMOD_LOCAL - - try { - $link = New-Item -ItemType SymbolicLink -Path $linkPath -Target $targetPath -Force -ErrorAction Stop - Write-Host "symbolic link created for $linkPath <<===>> $targetPath" -ForegroundColor Green - } - catch { - Write-Host "Failed to create symbolic link for $linkPath" -ForegroundColor Red - Write-Host "Error: $($_.Exception.Message)" -ForegroundColor DarkRed - } -} - -if ($steamInstallPath) { - $libraryFoldersPath = Join-Path -Path $steamInstallPath.InstallPath -ChildPath "steamapps\libraryfolders.vdf" - - if (Test-Path $libraryFoldersPath) { - $fileContent = Get-Content -Path $libraryFoldersPath - $vdf = [VdfDeserializer]::new(); - $result = $vdf.Deserialize($fileContent); - - $result.libraryfolders.PSObject.Properties | ForEach-Object { - $folder = $_.Value; - $folderPath = $folder.path -replace '\\\\', '\'; - $folderApps = $folder.apps; - - $folderApps.PSObject.Properties | ForEach-Object { - $appID = $_.Name - if($appID -eq $gmodAppID){ - $gmodAbsPath = Join-Path -Path $folderPath -ChildPath $gmodLocalPath; - $sdnAbsPath = $PWD.Path; - - Write-Host "Garry's Mod ($gmodAppID) found!" - Write-Host " Garry's Mod Path : $gmodAbsPath" - Write-Host " Source.NET/Game.Assets Path : $sdnAbsPath" - - Write-Host "Ensure that the Garry's Mod path points to the GarrysMod folder (not garrysmod!), and that the Source.NET path points to the Game.Assets folder."; - Write-Host 'Verify that this information looks correct, then press any key to continue.'; - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); - - # steam.inf used for networking compat - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\steam.inf" -GMOD_LOCAL "garrysmod\steam.inf" - - # Garry's Mod content - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\garrysmod_dir.vpk" -GMOD_LOCAL "garrysmod\garrysmod_dir.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\garrysmod_000.vpk" -GMOD_LOCAL "garrysmod\garrysmod_000.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\garrysmod_001.vpk" -GMOD_LOCAL "garrysmod\garrysmod_001.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\garrysmod_002.vpk" -GMOD_LOCAL "garrysmod\garrysmod_002.vpk" - - # HL2 content - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_dir.vpk" -GMOD_LOCAL "sourceengine\content_hl2_dir.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_000.vpk" -GMOD_LOCAL "sourceengine\content_hl2_000.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_001.vpk" -GMOD_LOCAL "sourceengine\content_hl2_001.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_002.vpk" -GMOD_LOCAL "sourceengine\content_hl2_002.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_003.vpk" -GMOD_LOCAL "sourceengine\content_hl2_003.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_004.vpk" -GMOD_LOCAL "sourceengine\content_hl2_004.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_005.vpk" -GMOD_LOCAL "sourceengine\content_hl2_005.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\content_hl2_006.vpk" -GMOD_LOCAL "sourceengine\content_hl2_006.vpk" - - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_misc_dir.vpk" -GMOD_LOCAL "sourceengine\hl2_misc_dir.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_misc_000.vpk" -GMOD_LOCAL "sourceengine\hl2_misc_000.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_misc_001.vpk" -GMOD_LOCAL "sourceengine\hl2_misc_001.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_misc_002.vpk" -GMOD_LOCAL "sourceengine\hl2_misc_002.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_misc_003.vpk" -GMOD_LOCAL "sourceengine\hl2_misc_003.vpk" - - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_sound_misc_dir.vpk" -GMOD_LOCAL "sourceengine\hl2_sound_misc_dir.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_sound_misc_000.vpk" -GMOD_LOCAL "sourceengine\hl2_sound_misc_000.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_sound_misc_001.vpk" -GMOD_LOCAL "sourceengine\hl2_sound_misc_001.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_sound_misc_002.vpk" -GMOD_LOCAL "sourceengine\hl2_sound_misc_002.vpk" - - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_dir.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_dir.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_000.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_000.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_001.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_001.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_002.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_002.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_003.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_003.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_004.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_004.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_005.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_005.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_006.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_006.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_007.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_007.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_008.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_008.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_009.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_009.vpk" - Link-SDN-Asset -SDN_ABS $sdnAbsPath -GMOD_ABS $gmodAbsPath -SDN_LOCAL "hl2\hl2_textures_010.vpk" -GMOD_LOCAL "sourceengine\hl2_textures_010.vpk" - - Write-Host "Done!"; - Write-Host -NoNewLine 'Press any key to exit.'; - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); - - exit - } - } - } - } else { - Write-Host "File not found: $libraryFoldersPath" - } -} else { - Write-Host "Steam install path not found." -} - -Write-Host "Something went wrong!"; -Write-Host -NoNewLine 'Press any key to exit.'; -$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); \ No newline at end of file diff --git a/Source.Engine/Common.cs b/Source.Engine/Common.cs index 910c47d3..ca05d97d 100644 --- a/Source.Engine/Common.cs +++ b/Source.Engine/Common.cs @@ -15,6 +15,8 @@ using static Source.Common.FilesystemHelpers; +using Game.Assets; + namespace Source.Engine; /// @@ -109,6 +111,8 @@ public void InitFilesystem(ReadOnlySpan fullModPath) { initInfo.LowViolence = Host.LowViolence; initInfo.MountHDContent = false; // Study this further + AssetLinker.CheckRequired(); + FileSystem.LoadSearchPaths(in initInfo); Gamedir = initInfo.ModPath ?? ""; @@ -146,7 +150,7 @@ static ReadOnlySpan Parse(ReadOnlySpan data) { if (data.IsEmpty) return null; - skipwhite: + skipwhite: while ((c = data[0]) <= ' ') { if (c == 0) return null; diff --git a/Source.Engine/Source.Engine.csproj b/Source.Engine/Source.Engine.csproj index 2fb650eb..353c0a92 100644 --- a/Source.Engine/Source.Engine.csproj +++ b/Source.Engine/Source.Engine.csproj @@ -13,6 +13,7 @@ + diff --git a/Source.GUI.Controls/Menu.cs b/Source.GUI.Controls/Menu.cs index 660dee50..5661dd69 100644 --- a/Source.GUI.Controls/Menu.cs +++ b/Source.GUI.Controls/Menu.cs @@ -870,9 +870,7 @@ public override void OnKillFocus(Panel? newPanel) { } } - internal void OnInternalMousePressed(Panel other, MouseButton code) { - // todo MenuMgr - } + internal void OnInternalMousePressed(Panel other, MouseButton code) => MenuManager.Instance.OnInternalMousePressed(other, code); public override void SetVisible(bool state) { if (state == IsVisible()) @@ -881,14 +879,15 @@ public override void SetVisible(bool state) { if (state == false) { PostActionSignal(KV_MenuClose); CloseOtherMenus(null); - SetCurrentlySelectedItem(-1); + + MenuManager.Instance.RemoveMenu(this); } else { MoveToFront(); RequestFocus(); - // MenuMgr + MenuManager.Instance.AddMenu(this); } base.SetVisible(state); @@ -1413,3 +1412,77 @@ public override void OnMessage(KeyValues message, IPanel? from) { base.OnMessage(message, from); } } + +class MenuManager +{ + private IVGuiInput Input = Singleton(); + private readonly List Menus = []; + public static MenuManager Instance = new(); + + public void AddMenu(Menu? menu) { + if (menu == null) + return; + + int count = Menus.Count; + for (int i = 0; i < count; i++) { + if (Menus[i] == menu) + return; + } + + Menus.Add(menu); + } + + public void RemoveMenu(Menu? menu) { + if (menu == null) + return; + + Menus.Remove(menu); + } + + public void OnInternalMousePressed(Panel other, MouseButton code) { + int count = Menus.Count; + if (count == 0) + return; + + Input.GetCursorPos(out int x, out int y); + + for (int i = 0; i < count; i++) { + Menu menu = Menus[i]; + + if (IsWithinMenuOrRelative(menu, x, y)) + return; + } + + AbortMenus(); + } + + private void AbortMenus() { + for (int i = 0; i < Menus.Count; i++) { + Menu menu = Menus[i]; + menu.SetVisible(false); + Menus.RemoveAt(i); + } + + Menus.Clear(); + } + + private bool IsWithinMenuOrRelative(Panel panel, int x, int y) { + IPanel? topMost = panel.IsWithinTraverse(x, y, true); + if (topMost != null) { + if (topMost == panel) + return true; + + if (topMost.HasParent(panel)) + return true; + } + + Panel? parent = panel.GetParent(); + if (parent != null) { + topMost = parent.IsWithinTraverse(x, y, true); + if (topMost == parent) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Source.Launcher/Program.cs b/Source.Launcher/Program.cs index 7f89ea97..355234ab 100644 --- a/Source.Launcher/Program.cs +++ b/Source.Launcher/Program.cs @@ -87,12 +87,12 @@ public void Boot() { // Let the engine builder take over and inject engine-specific dependencies .Build(dedicated: false); - // Generate our startup information - PreInit(); - // Start using this provider for the engine using ServiceLocatorScope locatorScope = new(engineAPI); + // Generate our startup information + PreInit(); + // Run the game var res = engineAPI.Run(); diff --git a/Source.Launcher/Source.Launcher.csproj b/Source.Launcher/Source.Launcher.csproj index 965073a3..acac3f1b 100644 --- a/Source.Launcher/Source.Launcher.csproj +++ b/Source.Launcher/Source.Launcher.csproj @@ -15,6 +15,7 @@ + @@ -33,7 +34,7 @@ - + diff --git a/Source.NET.sln b/Source.NET.sln index 43c1fd5b..42493df6 100644 --- a/Source.NET.sln +++ b/Source.NET.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11217.181 @@ -100,6 +100,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.ServerBrowser", "Game. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Source.Physics", "Source.Physics\Source.Physics.csproj", "{19D1DF3E-3FFA-406E-9F2F-4B793A3C6F09}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Assets", "Game.Assets\Game.Assets.csproj", "{97CBBC21-0181-4ED2-9CA6-115179E73290}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -232,6 +234,10 @@ Global {19D1DF3E-3FFA-406E-9F2F-4B793A3C6F09}.Debug|Any CPU.Build.0 = Debug|Any CPU {19D1DF3E-3FFA-406E-9F2F-4B793A3C6F09}.Release|Any CPU.ActiveCfg = Release|Any CPU {19D1DF3E-3FFA-406E-9F2F-4B793A3C6F09}.Release|Any CPU.Build.0 = Release|Any CPU + {97CBBC21-0181-4ED2-9CA6-115179E73290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97CBBC21-0181-4ED2-9CA6-115179E73290}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97CBBC21-0181-4ED2-9CA6-115179E73290}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97CBBC21-0181-4ED2-9CA6-115179E73290}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source.SDLManager/SourceDllMain.cs b/Source.SDLManager/SourceDllMain.cs index 1226b110..7b45403e 100644 --- a/Source.SDLManager/SourceDllMain.cs +++ b/Source.SDLManager/SourceDllMain.cs @@ -60,7 +60,7 @@ static unsafe bool SDLMessageBox(ReadOnlySpan title, ReadOnlySpan de SDL_MessageBoxData messageboxdata = default; SDL_MessageBoxButtonData* buttondata = stackalloc SDL_MessageBoxButtonData[2]; buttondata[0] = new() { buttonID = 1, flags = SDL_MessageBoxButtonFlags.SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, text = s_TextOK }; - buttondata[0] = new() { buttonID = 0, flags = SDL_MessageBoxButtonFlags.SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, text = s_TextCancel }; + buttondata[1] = new() { buttonID = 0, flags = SDL_MessageBoxButtonFlags.SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, text = s_TextCancel }; messageboxdata.window = g_SDLWindow; messageboxdata.title = (byte*)Marshal.StringToCoTaskMemUTF8(new(title));