├── src ├── HalfLife.UnifiedSdk.AssetSynchronizer │ ├── FileCopyItem.cs │ ├── Config │ │ ├── AssetPatternGroup.cs │ │ ├── ConfigException.cs │ │ ├── ManifestFilter.cs │ │ └── AssetManifest.cs │ ├── WatcherHelpers.cs │ ├── HalfLife.UnifiedSdk.AssetSynchronizer.csproj │ ├── FileCopier.cs │ └── Watcher.cs ├── HalfLife.UnifiedSdk.KeyValueMatcher │ ├── FlagsMatchMode.cs │ ├── PrintMode.cs │ ├── HalfLife.UnifiedSdk.KeyValueMatcher.csproj │ └── KeyValueMatcher.cs ├── HalfLife.UnifiedSdk.Packager │ ├── PackageDirectory.cs │ ├── PackagerOptions.cs │ ├── Config │ │ ├── ConfigException.cs │ │ ├── PackageDirectoryPath.cs │ │ ├── PackagePatternGroup.cs │ │ └── PackageManifest.cs │ └── HalfLife.UnifiedSdk.Packager.csproj ├── HalfLife.UnifiedSdk.MapCfgGenerator │ ├── Section.cs │ └── HalfLife.UnifiedSdk.MapCfgGenerator.csproj ├── HalfLife.UnifiedSdk.MapUpgraderDocGenerator │ ├── MarkdownMemberDocumentation.cs │ ├── XmlDocumentation.cs │ ├── XmlMemberDocumentation.cs │ ├── XmlSummaryElement.cs │ └── HalfLife.UnifiedSdk.MapUpgraderDocGenerator.csproj ├── HalfLife.UnifiedSdk.Utilities │ ├── Games │ │ ├── MapInfo.cs │ │ ├── GameEngine.cs │ │ └── MapCategory.cs │ ├── Maps │ │ ├── MapContentType.cs │ │ └── Map.cs │ ├── Entities │ │ ├── EntityEventArgs.cs │ │ ├── EntityKeyValueRemovingEventArgs.cs │ │ ├── RenderMode.cs │ │ ├── EntityKeyValueChangedEventArgs.cs │ │ └── EnumerableEntityExtensions.cs │ ├── Tools │ │ ├── TokenType.cs │ │ ├── UpgradeTool │ │ │ ├── MapUpgradeException.cs │ │ │ ├── GameSpecificMapUpgrade.cs │ │ │ ├── DelegatingMapUpgrade.cs │ │ │ ├── MapUpgrade.cs │ │ │ ├── MapUpgradeCollection.cs │ │ │ ├── MapUpgradeContext.cs │ │ │ ├── MapSpecificUpgrade.cs │ │ │ ├── MapUpgradeCollectionBuilder.cs │ │ │ └── MapUpgradeCommand.cs │ │ ├── MathUtilities.cs │ │ ├── SentenceUtilities.cs │ │ ├── Ripent.cs │ │ └── ScriptedSequenceUtilities.cs │ ├── Serialization │ │ ├── SledgeBSPFile │ │ │ ├── BSPMap.cs │ │ │ ├── BSPEntityList.cs │ │ │ ├── BSPSerializer.cs │ │ │ ├── BSPEntity.cs │ │ │ └── BSPMapBase.cs │ │ ├── InvalidFormatException.cs │ │ ├── SledgeMapFile │ │ │ ├── MapFileEntityList.cs │ │ │ ├── QuakeMapFileSerializer.cs │ │ │ ├── WorldcraftRmfMapFileSerializer.cs │ │ │ └── MapFileEntity.cs │ │ ├── EntFile │ │ │ ├── EntMap.cs │ │ │ └── EntSerializer.cs │ │ └── IMapSerializer.cs │ ├── Configuration │ │ └── PathConverter.cs │ ├── Logging │ │ ├── MapDiagnostics │ │ │ ├── DiagnosticsEventTypes.cs │ │ │ └── MapDiagnosticsBuilder.cs │ │ └── LoggerBinder.cs │ └── HalfLife.UnifiedSdk.Utilities.csproj ├── HalfLife.UnifiedSdk.Utilities.Tests │ ├── KeyValueUtilitiesTests.cs │ ├── HalfLife.UnifiedSdk.Utilities.Tests.csproj │ ├── EntityTests.cs │ └── MapFormatsTests.cs ├── HalfLife.UnifiedSdk.MapUpgrader.Upgrades │ ├── HalfLife.UnifiedSdk.MapUpgrader.Upgrades.csproj │ ├── DiagnosticsLevel.cs │ ├── GameConstants.cs │ ├── BlueShift │ │ ├── BaTram1FixSuitUpgrade.cs │ │ ├── BaPower2RemoveChapterTitleUpgrade.cs │ │ ├── BaTram1FixScientistHeadUpgrade.cs │ │ ├── BaTram2FixScientistSkinColorUpgrade.cs │ │ ├── BaOutroFixGruntsBodyUpgrade.cs │ │ ├── BaYard1FixDeadScientistModelUpgrade.cs │ │ ├── BaSecurity2ChangeHologramModelUpgrade.cs │ │ ├── RenameConsoleCivAnimationsUpgrade.cs │ │ ├── BaOutroDisableTriggerAutoUpgrade.cs │ │ ├── BaCanal1FixAlienSlaveSpawnsUpgrade.cs │ │ ├── RemapRosenbergNoUseFlagUpgrade.cs │ │ ├── BaYard4aSlavesUpgrade.cs │ │ ├── ChangeRosenbergModelUpgrade.cs │ │ └── ChangeBlueShiftSentencesUpgrade.cs │ ├── OpposingForce │ │ ├── Of1a1FixStretcherGunUpgrade.cs │ │ ├── RemoveGameModeSettingsUpgrade.cs │ │ ├── Of1a4bChangeLoaderSkinUpgrade.cs │ │ ├── Of6a5FixGenewormArriveSoundUpgrade.cs │ │ ├── ConvertSuitToPCVUpgrade.cs │ │ ├── Of5a2FixXenBullsquidScriptsUpgrade.cs │ │ ├── FixBlackOpsSpawnDelayUpgrade.cs │ │ ├── Of4a4BridgeUpgrade.cs │ │ ├── RenameOtisAnimationsUpgrade.cs │ │ ├── OfBoot1FixOspreyScriptUpgrade.cs │ │ ├── RenameBlackOpsAnimationsUpgrade.cs │ │ ├── Of0a0DisableItemDroppingUpgrade.cs │ │ ├── Of2a2FixGruntBodyUpgrade.cs │ │ ├── AdjustBlackOpsSkinUpgrade.cs │ │ ├── DisableFuncTankOfPersistenceUpgrade.cs │ │ ├── ConvertOtisBodyStateUpgrade.cs │ │ ├── ChangeFuncTankOfToFuncTankUpgrade.cs │ │ ├── MonsterTentacleSpawnFlagUpgrade.cs │ │ ├── RenameIntroGruntAnimationsUpgrade.cs │ │ └── ConvertScientistItemUpgrade.cs │ ├── HalfLife │ │ ├── C3a2FixLoadSavedUpgrade.cs │ │ ├── C4a2FixNihilanthDialogueUpgrade.cs │ │ ├── C4a3FixFlareSpritesUpgrade.cs │ │ ├── C2a5FixCrateGlobalNameUpgrade.cs │ │ ├── C3a2bFixWaterValvesUpgrade.cs │ │ └── C2a5FixBarrelPushTriggersUpgrade.cs │ ├── Common │ │ ├── RemoveDMDelayFromChargersUpgrade.cs │ │ ├── ChangeBell1SoundAndPitch.cs │ │ ├── AdjustMaxRangeUpgrade.cs │ │ ├── ConvertBarneyModelUpgrade.cs │ │ ├── SetCustomHullForGenericMonstersUpgrade.cs │ │ ├── FixRenderColorFormatUpgrade.cs │ │ ├── ReworkGamePlayerEquipUpgrade.cs │ │ ├── ConvertWorldItemsToItemUpgrade.cs │ │ ├── ConvertAngleToAnglesUpgrade.cs │ │ ├── ConvertWorldspawnGameTitleValueUpgrade.cs │ │ ├── FixNonLoopingSoundsUpgrade.cs │ │ ├── PruneExcessMultiManagerKeysUpgrade.cs │ │ ├── ConvertBreakableItemUpgrade.cs │ │ ├── RenameMessagesUpgrade.cs │ │ ├── RenameEntityClassNamesUpgrade.cs │ │ └── AdjustShotgunAnglesUpgrade.cs │ └── MapUpgradeToolFactory.cs ├── HalfLife.UnifiedSdk.ContentInstaller │ ├── GameInstallData.cs │ └── HalfLife.UnifiedSdk.ContentInstaller.csproj ├── HalfLife.UnifiedSdk.Formats │ ├── HalfLife.UnifiedSdk.Formats.csproj │ └── ConverterException.cs ├── HalfLife.UnifiedSdk.Bsp2Obj │ ├── TextureWriter.cs │ ├── HalfLife.UnifiedSdk.Bsp2Obj.csproj │ └── Program.cs ├── HalfLife.UnifiedSdk.MapUpgrader │ ├── HalfLife.UnifiedSdk.MapUpgrader.csproj │ └── Program.cs ├── HalfLife.UnifiedSdk.Hud2Json │ ├── HalfLife.UnifiedSdk.Hud2Json.csproj │ └── Program.cs ├── HalfLife.UnifiedSdk.Skill2Json │ ├── HalfLife.UnifiedSdk.Skill2Json.csproj │ └── Program.cs ├── HalfLife.UnifiedSdk.Materials2Json │ ├── HalfLife.UnifiedSdk.Materials2Json.csproj │ └── Program.cs └── HalfLife.UnifiedSdk.Sentences2Json │ ├── HalfLife.UnifiedSdk.Sentences2Json.csproj │ └── Program.cs └── LICENSE /src/HalfLife.UnifiedSdk.AssetSynchronizer/FileCopyItem.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.AssetSynchronizer 2 | { 3 | internal record struct FileCopyItem(Watcher Watcher, string FileName); 4 | } 5 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.KeyValueMatcher/FlagsMatchMode.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.KeyValueMatcher 2 | { 3 | public enum FlagsMatchMode 4 | { 5 | Inclusive = 0, 6 | Exclusive 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/PackageDirectory.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Packager 2 | { 3 | internal sealed record PackageDirectory(string Path, IEnumerable IncludePatterns, IEnumerable ExcludePatterns); 4 | } 5 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapCfgGenerator/Section.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace HalfLife.UnifiedSdk.MapCfgGenerator 4 | { 5 | internal sealed record Section(Action WriterCallback, string Condition = ""); 6 | } 7 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgraderDocGenerator/MarkdownMemberDocumentation.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.MapUpgraderDocGenerator 2 | { 3 | internal sealed record MarkdownMemberDocumentation(string Category, string ClassName, string Summary); 4 | } 5 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Games/MapInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Games 2 | { 3 | /// Provides information about a map, such as which type of map it is. 4 | public record MapInfo(string Name, MapCategory Category); 5 | } 6 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/PackagerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Packager 2 | { 3 | internal sealed record PackagerOptions(string PackageName, string RootDirectory, IEnumerable Directories) 4 | { 5 | public bool ListOmittedFiles { get; init; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgraderDocGenerator/XmlDocumentation.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace HalfLife.UnifiedSdk.MapUpgraderDocGenerator 4 | { 5 | [XmlRoot("doc", IsNullable = false)] 6 | public sealed class XmlDocumentation 7 | { 8 | [XmlArray("members")] 9 | [XmlArrayItem("member")] 10 | public List Members { get; set; } = new(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgraderDocGenerator/XmlMemberDocumentation.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace HalfLife.UnifiedSdk.MapUpgraderDocGenerator 4 | { 5 | public sealed class XmlMemberDocumentation 6 | { 7 | [XmlAttribute("name")] 8 | public string Name { get; set; } = string.Empty; 9 | 10 | [XmlElement("summary")] 11 | public XmlSummaryElement Summary { get; set; } = new(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities.Tests/KeyValueUtilitiesTests.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using Xunit; 3 | 4 | namespace HalfLife.UnifiedSdk.Utilities.Tests 5 | { 6 | public class KeyValueUtilitiesTests 7 | { 8 | [Fact] 9 | public void EmptyMapString_ResultMatches() 10 | { 11 | Assert.Equal(@"{ 12 | ""classname"" ""worldspawn"" 13 | } 14 | ", 15 | KeyValueUtilities.EmptyMapString); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Games/GameEngine.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Games 2 | { 3 | /// Game engine identifiers. 4 | public enum GameEngine 5 | { 6 | /// GoldSource engine. Also known as the Half-Life (1) engine, Valve Game Engine. 7 | GoldSource = 0, 8 | 9 | /// Source engine. 10 | Source, 11 | 12 | /// Source 2 engine. 13 | Source2 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/Config/AssetPatternGroup.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Configuration; 2 | using Newtonsoft.Json; 3 | 4 | namespace HalfLife.UnifiedSdk.AssetSynchronizer.Config 5 | { 6 | internal sealed class AssetPatternGroup 7 | { 8 | [JsonProperty(Required = Required.Always, ItemConverterType = typeof(PathConverter))] 9 | public string Path { get; set; } = string.Empty; 10 | 11 | public List Filters { get; set; } = new(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/Config/ConfigException.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Packager.Config 2 | { 3 | internal sealed class ConfigException : Exception 4 | { 5 | public ConfigException() 6 | { 7 | } 8 | 9 | public ConfigException(string? message) 10 | : base(message) 11 | { 12 | } 13 | 14 | public ConfigException(string? message, Exception? innerException) 15 | : base(message, innerException) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife.UnifiedSdk.MapUpgrader.Upgrades.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | True 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Games/MapCategory.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Games 2 | { 3 | /// Category that a map belongs to. 4 | public enum MapCategory 5 | { 6 | /// Campaign maps (c*a*, of*a*, ba_*, etc). 7 | Campaign = 0, 8 | 9 | /// Training maps (Hazard course, Boot camp). 10 | Training, 11 | 12 | /// Multiplayer maps. Also includes co-op and CTF maps. 13 | Multiplayer 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Maps/MapContentType.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Maps 2 | { 3 | /// The type of content a map has. 4 | public enum MapContentType 5 | { 6 | /// A map source, like .map. 7 | Source = 0, 8 | 9 | /// 10 | /// A compiled map, like .bsp or .ent. 11 | /// Compiled maps have a model key for brush entities filled in with the brush model index. 12 | /// 13 | Compiled 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.KeyValueMatcher/PrintMode.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.KeyValueMatcher 2 | { 3 | /// 4 | /// What to print when a match is found. 5 | /// 6 | internal enum PrintMode 7 | { 8 | /// 9 | /// Print nothing. 10 | /// 11 | Nothing = 0, 12 | 13 | /// 14 | /// Print the key and value. 15 | /// 16 | KeyValue, 17 | 18 | /// 19 | /// Print the entire entity. 20 | /// 21 | Entity 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Entities/EntityEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Entities 4 | { 5 | /// Event arguments type for anything involving entities. 6 | public class EntityEventArgs : EventArgs 7 | { 8 | /// The entity involved in this event. 9 | public Entity Entity { get; } 10 | 11 | /// Creates a new instance of this event object. 12 | public EntityEventArgs(Entity entity) 13 | { 14 | Entity = entity; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/Config/ConfigException.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.AssetSynchronizer.Config 2 | { 3 | // TODO: should refactor packager and this to use common code 4 | internal sealed class ConfigException : Exception 5 | { 6 | public ConfigException() 7 | { 8 | } 9 | 10 | public ConfigException(string? message) 11 | : base(message) 12 | { 13 | } 14 | 15 | public ConfigException(string? message, Exception? innerException) 16 | : base(message, innerException) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Tools 2 | { 3 | /// 4 | /// Possible values for the token type. 5 | /// 6 | public enum TokenType 7 | { 8 | /// 9 | /// No token was parsed. 10 | /// 11 | None = 0, 12 | 13 | /// 14 | /// A valid token was parsed. 15 | /// 16 | Text, 17 | 18 | /// 19 | /// A comment was parsed. 20 | /// 21 | Comment 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/DiagnosticsLevel.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades 2 | { 3 | /// 4 | /// Log output diagnostics levels. 5 | /// 6 | public enum DiagnosticsLevel 7 | { 8 | /// 9 | /// No diagnostics output. 10 | /// 11 | Disabled = 0, 12 | 13 | /// 14 | /// All diagnostics output, excluding verbose output. 15 | /// 16 | Common, 17 | 18 | /// 19 | /// All diagnostics output. 20 | /// 21 | All 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Entities/EntityKeyValueRemovingEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Entities 2 | { 3 | /// 4 | /// Event arguments type for entity keyvalue removed event. 5 | /// 6 | public class EntityKeyValueRemovingEventArgs : EntityEventArgs 7 | { 8 | /// Key of the keyvalue that was removed. 9 | public string Key { get; } 10 | 11 | /// Creates a new instance of this event object. 12 | public EntityKeyValueRemovingEventArgs(Entity entity, string key) 13 | : base(entity) 14 | { 15 | Key = key; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/GameConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades 4 | { 5 | /// 6 | /// Constants used by the game. 7 | /// TODO: should probably be in a separate assembly. 8 | /// 9 | internal static class GameConstants 10 | { 11 | /// 12 | /// Player hull minimum bounds. 13 | /// 14 | public static readonly Vector3 HullMin = new(-16, -16, -36); 15 | 16 | /// 17 | /// Player hull maximum bounds. 18 | /// 19 | public static readonly Vector3 HullMax = new(16, 16, 36); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Entities/RenderMode.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Entities 2 | { 3 | /// 4 | /// Possible values for rendermode keyvalue. 5 | /// 6 | public enum RenderMode 7 | { 8 | /// src 9 | RenderNormal, 10 | /// c*a+dest*(1-a) 11 | RenderTransColor, 12 | /// src*a+dest*(1-a) 13 | RenderTransTexture, 14 | /// src*a+dest -- No Z buffer checks 15 | RenderGlow, 16 | /// src*srca+dest*(1-srca) 17 | RenderTransAlpha, 18 | /// src*a+dest 19 | RenderTransAdd 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaTram1FixSuitUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Removes the HEV suit from ba_tram1 (now given by map config). 8 | /// 9 | internal sealed class BaTram1FixSuitUpgrade : MapSpecificUpgrade 10 | { 11 | public BaTram1FixSuitUpgrade() 12 | : base("ba_tram1") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | context.Map.Entities.RemoveAllOfClass("item_suit"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeBSPFile/BSPMap.cs: -------------------------------------------------------------------------------- 1 | using Sledge.Formats.Bsp; 2 | using System.IO; 3 | 4 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeBSPFile 5 | { 6 | internal sealed class BSPMap : BSPMapBase 7 | { 8 | private readonly BspFile _bspFile; 9 | 10 | internal BSPMap(string fileName, BspFile bspFile) 11 | : base(fileName, 12 | bspFile.GetLump() ?? throw new InvalidFormatException("Missing entities lump")) 13 | { 14 | _bspFile = bspFile; 15 | } 16 | 17 | public override void Serialize(Stream stream) 18 | { 19 | _bspFile.WriteToStream(stream, _bspFile.Version); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapUpgradeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 4 | { 5 | /// Represents errors that occur during the upgrading of a map. 6 | public sealed class MapUpgradeException : Exception 7 | { 8 | /// 9 | public MapUpgradeException() 10 | { 11 | } 12 | 13 | /// 14 | public MapUpgradeException(string? message) 15 | : base(message) 16 | { 17 | } 18 | 19 | /// 20 | public MapUpgradeException(string? message, Exception? innerException) 21 | : base(message, innerException) 22 | { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeBSPFile/BSPEntityList.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeBSPFile 4 | { 5 | internal sealed class BSPEntityList : EntityList 6 | { 7 | private readonly BSPMapBase _map; 8 | 9 | public BSPEntityList(BSPMapBase map) 10 | : base(map.GetEntities) 11 | { 12 | _map = map; 13 | } 14 | 15 | protected override Entity CreateNewEntityCore(string className) 16 | { 17 | return _map.CreateNewEntity(className); 18 | } 19 | 20 | protected override void RemoveAtCore(Entity entity, int index) 21 | { 22 | _map.Remove(entity); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaPower2RemoveChapterTitleUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 2 | 3 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 4 | { 5 | /// 6 | /// Removes the chaptertitle key from worldspawn in ba_power2 to remove the redundant chapter title text. 7 | /// 8 | internal sealed class BaPower2RemoveChapterTitleUpgrade : MapSpecificUpgrade 9 | { 10 | public BaPower2RemoveChapterTitleUpgrade() 11 | : base("ba_power2") 12 | { 13 | } 14 | 15 | protected override void ApplyCore(MapUpgradeContext context) 16 | { 17 | context.Map.Entities.Worldspawn.Remove("chaptertitle"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/InvalidFormatException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Serialization 4 | { 5 | /// 6 | /// Represents errors that occur during map loading. 7 | /// 8 | public sealed class InvalidFormatException : Exception 9 | { 10 | /// 11 | public InvalidFormatException() 12 | { 13 | } 14 | 15 | /// 16 | public InvalidFormatException(string? message) 17 | : base(message) 18 | { 19 | } 20 | 21 | /// 22 | public InvalidFormatException(string? message, Exception? innerException) 23 | : base(message, innerException) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeMapFile/MapFileEntityList.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeMapFile 4 | { 5 | internal sealed class MapFileEntityList : EntityList 6 | { 7 | private readonly MapFileMap _map; 8 | 9 | public MapFileEntityList(MapFileMap map) 10 | : base(map.GetEntities) 11 | { 12 | _map = map; 13 | } 14 | 15 | protected override Entity CreateNewEntityCore(string className) 16 | { 17 | return _map.CreateNewEntity(className); 18 | } 19 | 20 | protected override void RemoveAtCore(Entity entity, int index) 21 | { 22 | _map.Remove(entity); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of1a1FixStretcherGunUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Updates the stretcher grunt's body value to make the grunt's weapon invisible. 8 | /// 9 | internal sealed class Of1a1FixStretcherGunUpgrade : MapSpecificUpgrade 10 | { 11 | public Of1a1FixStretcherGunUpgrade() 12 | : base("of1a1") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | context.Map.Entities.Find("stretcher_grunt")?.SetInteger("body", 17); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaTram1FixScientistHeadUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Removes the body keyvalue from the monster_generic newspaper scientist in ba_tram1. 8 | /// 9 | internal sealed class BaTram1FixScientistHeadUpgrade : MapSpecificUpgrade 10 | { 11 | public BaTram1FixScientistHeadUpgrade() 12 | : base("ba_tram1") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | context.Map.Entities.Find("sitter")?.Remove("body"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/Config/PackageDirectoryPath.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Configuration; 2 | using Newtonsoft.Json; 3 | 4 | namespace HalfLife.UnifiedSdk.Packager.Config 5 | { 6 | internal sealed class PackageDirectoryPath 7 | { 8 | /// 9 | /// Path to the directory to package. 10 | /// This is a path relative to the game directory. 11 | /// The symbol %ModDirectory% is replaced with the mod directory. 12 | /// 13 | [JsonProperty(Required = Newtonsoft.Json.Required.Always, ItemConverterType = typeof(PathConverter))] 14 | public string Path { get; set; } = string.Empty; 15 | 16 | [JsonProperty(Required = Newtonsoft.Json.Required.DisallowNull)] 17 | public bool Required { get; set; } = true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife/C3a2FixLoadSavedUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.HalfLife 5 | { 6 | /// 7 | /// Increases the reload delay after killing the scientist in c3a2 8 | /// to allow the entire game over message to display. 9 | /// 10 | internal sealed class C3a2FixLoadSavedUpgrade : MapSpecificUpgrade 11 | { 12 | public C3a2FixLoadSavedUpgrade() 13 | : base("c3a2") 14 | { 15 | } 16 | 17 | protected override void ApplyCore(MapUpgradeContext context) 18 | { 19 | context.Map.Entities.Find("c3a2_restart")?.SetInteger("loadtime", 10); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeMapFile/QuakeMapFileSerializer.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Maps; 2 | using Sledge.Formats.Map.Formats; 3 | using System.IO; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeMapFile 6 | { 7 | internal sealed class QuakeMapFileSerializer : IMapSerializer 8 | { 9 | private readonly QuakeMapFormat _format = new(); 10 | 11 | public string Extension { get; } 12 | 13 | public QuakeMapFileSerializer() 14 | { 15 | Extension = "." + _format.Extension; 16 | } 17 | 18 | public Map Deserialize(string fileName, Stream stream) 19 | { 20 | var mapFile = _format.Read(stream); 21 | 22 | return new MapFileMap(fileName, mapFile, _format, "Worldcraft"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.ContentInstaller/GameInstallData.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | 3 | namespace HalfLife.UnifiedSdk.ContentInstaller 4 | { 5 | /// Defines a single game as a source of content. 6 | /// object containing information about the game. 7 | /// 8 | /// If not null, this action will be invoked to perform any additional copy steps. 9 | /// Provides the source and destination mod directories 10 | /// If a missing installation is logged as a warning. 11 | internal sealed record class GameInstallData( 12 | GameInfo Info, 13 | Action? AdditionalCopySteps = null, 14 | bool IsRequired = true); 15 | } 16 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/RemoveGameModeSettingsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Removes the CTF game mode settings from Opposing Force maps. 8 | /// 9 | internal sealed class RemoveGameModeSettingsUpgrade : GameSpecificMapUpgrade 10 | { 11 | public RemoveGameModeSettingsUpgrade() 12 | : base(ValveGames.OpposingForce) 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | var worldspawn = context.Map.Entities.Worldspawn; 19 | 20 | worldspawn.Remove("defaultctf"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/WatcherHelpers.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | 3 | namespace HalfLife.UnifiedSdk.AssetSynchronizer 4 | { 5 | internal static class WatcherHelpers 6 | { 7 | public static void PrintException(ILogger logger, Exception? ex) 8 | { 9 | if (ex is not null) 10 | { 11 | logger.Error("Message: {Message}", ex.Message); 12 | logger.Error("Stacktrace:"); 13 | 14 | if (ex.StackTrace is not null) 15 | { 16 | logger.Error("{StackTrace}", ex.StackTrace); 17 | } 18 | else 19 | { 20 | logger.Error("No stack trace"); 21 | } 22 | 23 | PrintException(logger, ex.InnerException); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeMapFile/WorldcraftRmfMapFileSerializer.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Maps; 2 | using Sledge.Formats.Map.Formats; 3 | using System.IO; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeMapFile 6 | { 7 | internal sealed class WorldcraftRmfMapFileSerializer : IMapSerializer 8 | { 9 | private readonly WorldcraftRmfFormat _format = new(); 10 | 11 | public string Extension { get; } 12 | 13 | public WorldcraftRmfMapFileSerializer() 14 | { 15 | Extension = "." + _format.Extension; 16 | } 17 | 18 | public Map Deserialize(string fileName, Stream stream) 19 | { 20 | var mapFile = _format.Read(stream); 21 | 22 | return new MapFileMap(fileName, mapFile, _format, "2.2"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaTram2FixScientistSkinColorUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Fixes the Luther scientist in ba_tram2's reflective lab room having white hands. 8 | /// 9 | internal sealed class BaTram2FixScientistSkinColorUpgrade : MapSpecificUpgrade 10 | { 11 | public BaTram2FixScientistSkinColorUpgrade() 12 | : base("ba_tram2") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | context.Map.Entities.Find("joey_normal")?.SetInteger("skin", 1); 19 | context.Map.Entities.Find("joey_reflect")?.SetInteger("skin", 1); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of1a4bChangeLoaderSkinUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Changes the loader entity's skin in of1a4b to use the correct crate texture. 8 | /// 9 | internal sealed class Of1a4bChangeLoaderSkinUpgrade : MapSpecificUpgrade 10 | { 11 | public Of1a4bChangeLoaderSkinUpgrade() 12 | : base("of1a4b") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | foreach (var loader in context.Map.Entities.OfClass("monster_op4loader")) 19 | { 20 | loader.SetInteger("skin", 1); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of6a5FixGenewormArriveSoundUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Adds a missing .wav extension to the sound played when the Geneworm enters in of6a5. 8 | /// 9 | internal sealed class Of6a5FixGenewormArriveSoundUpgrade : MapSpecificUpgrade 10 | { 11 | public Of6a5FixGenewormArriveSoundUpgrade() 12 | : base("of6a5") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | var sound = context.Map.Entities.Find("genearrivesound"); 19 | 20 | sound?.SetString("message", "ambience/port_suckout1.wav"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/EntFile/EntMap.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Serialization.SledgeBSPFile; 2 | using Sledge.Formats.Bsp; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.EntFile 7 | { 8 | /// A Ripent .ent file. Contains only entity data in compiled form. 9 | internal sealed class EntMap : BSPMapBase 10 | { 11 | /// 12 | public EntMap(string fileName, Sledge.Formats.Bsp.Lumps.Entities entitiesLump) 13 | : base(fileName, entitiesLump) 14 | { 15 | } 16 | 17 | /// 18 | public override void Serialize(Stream stream) 19 | { 20 | using var writer = new BinaryWriter(stream, Encoding.UTF8, true); 21 | _entitiesLump.Write(writer, Version.Goldsource); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/EntFile/EntSerializer.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Maps; 2 | using Sledge.Formats.Bsp; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.EntFile 7 | { 8 | internal sealed class EntSerializer : IMapSerializer 9 | { 10 | public string Extension => ".ent"; 11 | 12 | public Map Deserialize(string fileName, Stream stream) 13 | { 14 | var entitiesLump = new Sledge.Formats.Bsp.Lumps.Entities(); 15 | 16 | using var reader = new BinaryReader(stream, Encoding.UTF8, true); 17 | entitiesLump.Read(reader, new Blob 18 | { 19 | Offset = 0, 20 | Length = (int)stream.Length 21 | }, 22 | Version.Goldsource); 23 | 24 | return new EntMap(fileName, entitiesLump); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife/C4a2FixNihilanthDialogueUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.HalfLife 5 | { 6 | /// 7 | /// Fixes Nihilanth's dialogue not playing at the start of c4a2 (Gonarch's Lair). 8 | /// 9 | internal sealed class C4a2FixNihilanthDialogueUpgrade : MapSpecificUpgrade 10 | { 11 | public C4a2FixNihilanthDialogueUpgrade() 12 | : base("c4a2") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | // The sound is set to play on map start; this turns it off. 19 | context.Map.Entities.FirstOrDefault(e => e.GetTarget() == "c4a2_startaudio")?.SetInteger("triggerstate", 1); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeBSPFile/BSPSerializer.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Maps; 2 | using Sledge.Formats.Bsp; 3 | using System.IO; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeBSPFile 6 | { 7 | internal sealed class BSPSerializer : IMapSerializer 8 | { 9 | public string Extension => ".bsp"; 10 | 11 | public Map Deserialize(string fileName, Stream stream) 12 | { 13 | var bspFile = new BspFile(stream); 14 | 15 | //Force this off so bsp files saved to disk can be loaded when used in normal maps. 16 | //This means Blue Shift maps can't be saved back to the original game directory, 17 | //but that's not something you should be doing with this tool. 18 | bspFile.Options.UseBlueShiftFormat = false; 19 | 20 | return new BSPMap(fileName, bspFile); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Formats/HalfLife.UnifiedSdk.Formats.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | True 16 | True 17 | Resources.resx 18 | 19 | 20 | 21 | 22 | 23 | ResXFileCodeGenerator 24 | Resources.Designer.cs 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/RemoveDMDelayFromChargersUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 2 | using System.Collections.Immutable; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 5 | { 6 | /// 7 | /// Removes the dmdelay keyvalue from charger entities. The original game ignores these. 8 | /// 9 | internal sealed class RemoveDMDelayFromChargersUpgrade : MapUpgrade 10 | { 11 | private static readonly ImmutableArray ClassNames = ImmutableArray.Create( 12 | "func_healthcharger", 13 | "func_recharge"); 14 | 15 | protected override void ApplyCore(MapUpgradeContext context) 16 | { 17 | foreach (var entity in context.Map.Entities.Where(e => ClassNames.Contains(e.ClassName))) 18 | { 19 | entity.Remove("dmdelay"); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/ConvertSuitToPCVUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Converts item_suit's model to use w_pcv.mdl in Opposing Force maps. 9 | /// 10 | internal sealed class ConvertSuitToPCVUpgrade : GameSpecificMapUpgrade 11 | { 12 | public ConvertSuitToPCVUpgrade() 13 | : base(ValveGames.OpposingForce) 14 | { 15 | } 16 | 17 | protected override void ApplyCore(MapUpgradeContext context) 18 | { 19 | foreach (var entity in context.Map.Entities.OfClass("item_suit")) 20 | { 21 | entity.SetModel("models/w_pcv.mdl"); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Configuration/PathConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.IO; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Configuration 6 | { 7 | /// 8 | /// Converts paths to and from platform-specific form. 9 | /// 10 | public sealed class PathConverter : JsonConverter 11 | { 12 | /// 13 | public override void WriteJson(JsonWriter writer, string? value, JsonSerializer serializer) 14 | { 15 | var path = value?.Replace(Path.DirectorySeparatorChar, '/'); 16 | writer.WriteValue(path); 17 | } 18 | 19 | /// 20 | public override string? ReadJson(JsonReader reader, Type objectType, string? existingValue, bool hasExistingValue, JsonSerializer serializer) 21 | { 22 | return ((string?)reader.Value)?.Replace('/', Path.DirectorySeparatorChar); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of5a2FixXenBullsquidScriptsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Fixes the Bullsquids in of5a2 having the wrong targetnames causing the eating scripts to fail. 8 | /// 9 | internal sealed class Of5a2FixXenBullsquidScriptsUpgrade : MapSpecificUpgrade 10 | { 11 | public Of5a2FixXenBullsquidScriptsUpgrade() 12 | : base("of5a2") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | var feeder1 = context.Map.Entities.Find("t27"); 19 | var feeder2 = context.Map.Entities.Find("t6"); 20 | 21 | feeder1?.SetTargetName("feeder_1"); 22 | feeder2?.SetTargetName("feeder_2"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/FixBlackOpsSpawnDelayUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Sets the assassin4_spawn monstermaker in of6a1 to spawn a Black Ops assassin immediately 8 | /// to make the switch from prisoner less obvious. 9 | /// 10 | internal sealed class FixBlackOpsSpawnDelayUpgrade : MapSpecificUpgrade 11 | { 12 | public FixBlackOpsSpawnDelayUpgrade() 13 | : base("of6a1") 14 | { 15 | } 16 | 17 | protected override void ApplyCore(MapUpgradeContext context) 18 | { 19 | var monstermaker = context.Map.Entities.FirstOrDefault(e => e.ClassName == "monstermaker" && e.GetTargetName() == "assassin4_spawn"); 20 | 21 | monstermaker?.SetDouble("delay", 0); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaOutroFixGruntsBodyUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Adjusts the body values of the Human Grunts in ba_outro to match the Half-Life script 8 | /// and to avoid clipping weapons into Freeman. 9 | /// 10 | internal sealed class BaOutroFixGruntsBodyUpgrade : MapSpecificUpgrade 11 | { 12 | public BaOutroFixGruntsBodyUpgrade() 13 | : base("ba_outro") 14 | { 15 | } 16 | 17 | protected override void ApplyCore(MapUpgradeContext context) 18 | { 19 | var grunt1 = context.Map.Entities.Find("drag_grunt1"); 20 | var grunt2 = context.Map.Entities.Find("drag_grunt2"); 21 | 22 | grunt1?.SetInteger("body", 4); 23 | grunt2?.SetInteger("body", 1); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ChangeBell1SoundAndPitch.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 5 | { 6 | /// 7 | /// Find all buttons/bell1.wav sounds that have a pitch set to 80. 8 | /// Change those to use an alternative sound and set their pitch to 100. 9 | /// 10 | internal class ChangeBell1SoundAndPitch : MapUpgrade 11 | { 12 | protected override void ApplyCore(MapUpgradeContext context) 13 | { 14 | foreach (var entity in context.Map.Entities 15 | .OfClass("ambient_generic") 16 | .WhereString("message", "buttons/bell1.wav") 17 | .Where(e => e.GetInteger("pitch") == 80)) 18 | { 19 | entity.SetString("message", "buttons/bell1_alt.wav"); 20 | entity.SetInteger("pitch", 100); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of4a4BridgeUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Fixes the Pit Worm's Nest bridge possibly breaking if triggered too soon. 8 | /// 9 | internal sealed class Of4a4BridgeUpgrade : MapSpecificUpgrade 10 | { 11 | public Of4a4BridgeUpgrade() 12 | : base("of4a4") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | var manager = context.Map.Entities.FirstOrDefault(e => e.GetTargetName() == "kill_pitworm_mm"); 19 | 20 | if (manager is null) 21 | { 22 | return; 23 | } 24 | 25 | manager.SetInteger("bridge_global_trigger", 30); 26 | manager.SetInteger("bridge_mm", 30); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Entities/EntityKeyValueChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Entities 2 | { 3 | /// 4 | /// Event arguments type for entity keyvalue changed event. 5 | /// 6 | public class EntityKeyValueChangedEventArgs : EntityEventArgs 7 | { 8 | /// Key of the keyvalue that was changed. 9 | public string Key { get; } 10 | 11 | /// Previous value. 12 | public string? PreviousValue { get; } 13 | 14 | /// Current value. 15 | public string CurrentValue { get; } 16 | 17 | /// Creates a new instance of this event object. 18 | public EntityKeyValueChangedEventArgs(Entity entity, string key, string? previousValue, string currentValue) 19 | : base(entity) 20 | { 21 | Key = key; 22 | PreviousValue = previousValue; 23 | CurrentValue = currentValue; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaYard1FixDeadScientistModelUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Changes incorrect dead scientist head (Rosenberg) in ba_yard1 to use the same as in ba_yard4 (Glasses). 8 | /// 9 | internal sealed class BaYard1FixDeadScientistModelUpgrade : MapSpecificUpgrade 10 | { 11 | public BaYard1FixDeadScientistModelUpgrade() 12 | : base("ba_yard1") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | foreach (var entity in context.Map.Entities 19 | .OfClass("monster_scientist_dead") 20 | .Where(e => e.GetInteger("body") == 3)) 21 | { 22 | entity.SetInteger("body", 0); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife/C4a3FixFlareSpritesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.HalfLife 5 | { 6 | /// 7 | /// Fixes the flare sprites shown during Nihilanth's death script using the wrong render mode. 8 | /// 9 | internal sealed class C4a3FixFlareSpritesUpgrade : MapSpecificUpgrade 10 | { 11 | public C4a3FixFlareSpritesUpgrade() 12 | : base("c4a3") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | foreach (var entity in context.Map.Entities 19 | .OfClass("env_sprite") 20 | .WhereString("model", "sprites/XFlare3.spr")) 21 | { 22 | entity.SetRenderMode(RenderMode.RenderTransAdd); 23 | entity.SetRenderAmount(255); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/GameSpecificMapUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 4 | { 5 | /// 6 | /// Helper class to apply an upgrade to a specific game. 7 | /// 8 | public abstract class GameSpecificMapUpgrade : MapUpgrade 9 | { 10 | /// 11 | /// The game that this upgrade applies to. 12 | /// 13 | public GameInfo GameInfo { get; } 14 | 15 | /// 16 | /// Creates an upgrade that applies only to the specified game. 17 | /// 18 | /// 19 | protected GameSpecificMapUpgrade(GameInfo gameInfo) 20 | { 21 | GameInfo = gameInfo; 22 | } 23 | 24 | /// 25 | protected override bool Filter(MapUpgradeContext context) 26 | { 27 | return context.GameInfo == GameInfo; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgraderDocGenerator/XmlSummaryElement.cs: -------------------------------------------------------------------------------- 1 | using System.Xml; 2 | using System.Xml.Schema; 3 | using System.Xml.Serialization; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgraderDocGenerator 6 | { 7 | public sealed class XmlSummaryElement : IXmlSerializable 8 | { 9 | public string Contents { get; set; } = string.Empty; 10 | 11 | public XmlSchema? GetSchema() 12 | { 13 | return null; 14 | } 15 | 16 | public void ReadXml(XmlReader reader) 17 | { 18 | // Read the contents as a single text string. 19 | // This is needed because some summary tags contain text that uses XML tags for styling 20 | // which XmlSerializer can't deal with. 21 | // Note that this removes whitespace between adjacent tags which we add back later. 22 | Contents = reader.ReadInnerXml().Trim(); 23 | } 24 | 25 | public void WriteXml(XmlWriter writer) 26 | { 27 | throw new NotSupportedException(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaSecurity2ChangeHologramModelUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Changes Gina model in ba_security2 to allow playing push cart sequence. 8 | /// 9 | internal sealed class BaSecurity2ChangeHologramModelUpgrade : MapSpecificUpgrade 10 | { 11 | public BaSecurity2ChangeHologramModelUpgrade() 12 | : base("ba_security2") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | if (context.Map.Entities.Find("gina_push") is { } gina) 19 | { 20 | gina.SetModel("models/holo_cart.mdl"); 21 | gina.SetVector3("custom_hull_min", GameConstants.HullMin); 22 | gina.SetVector3("custom_hull_max", GameConstants.HullMax); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/DelegatingMapUpgrade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 4 | { 5 | /// 6 | /// An upgrade that invokes the given delegate to apply upgrades. 7 | /// 8 | public sealed class DelegatingMapUpgrade : MapUpgrade 9 | { 10 | private readonly Action _upgrade; 11 | 12 | /// 13 | /// Creates a new delegating upgrade. 14 | /// 15 | /// Delegate to invoke on apply. 16 | /// If is null. 17 | public DelegatingMapUpgrade(Action upgrade) 18 | { 19 | _upgrade = upgrade ?? throw new ArgumentNullException(nameof(upgrade)); 20 | } 21 | 22 | /// 23 | protected override void ApplyCore(MapUpgradeContext context) 24 | { 25 | _upgrade(context); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/AdjustMaxRangeUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Adjusts the MaxRange keyvalue for specific maps to fix graphical issues 9 | /// when geometry is further away than the original value. 10 | /// 11 | internal sealed class AdjustMaxRangeUpgrade : MapUpgrade 12 | { 13 | private static readonly ImmutableDictionary MapsToAdjust = new Dictionary 14 | { 15 | ["c2a2a"] = 8192 16 | }.ToImmutableDictionary(); 17 | 18 | protected override void ApplyCore(MapUpgradeContext context) 19 | { 20 | if (MapsToAdjust.TryGetValue(context.Map.BaseName, out var value)) 21 | { 22 | context.Map.Entities.Worldspawn.SetInteger("MaxRange", value); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/RenameOtisAnimationsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Renames certain animations referenced by scripted_sequences targeting monster_otis 9 | /// or entities using its model to use the new animation names. 10 | /// 11 | internal sealed class RenameOtisAnimationsUpgrade : MapUpgrade 12 | { 13 | private static readonly ImmutableDictionary AnimationRemap = new Dictionary 14 | { 15 | { "fence", "otis_fence" }, 16 | { "wave", "otis_wave" } 17 | }.ToImmutableDictionary(); 18 | 19 | protected override void ApplyCore(MapUpgradeContext context) 20 | { 21 | ScriptedSequenceUtilities.RenameAnimations(context, "monster_otis", "models/otis.mdl", AnimationRemap); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sam Vanheer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/OfBoot1FixOspreyScriptUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Prevents the Osprey in ofboot1 from switching to the rotor animation 8 | /// and falling through the ground after loading a save game. 9 | /// 10 | internal sealed class OfBoot1FixOspreyScriptUpgrade : MapSpecificUpgrade 11 | { 12 | public OfBoot1FixOspreyScriptUpgrade() 13 | : base("ofboot1") 14 | { 15 | } 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | var script = context.Map.Entities 19 | .FirstOrDefault(e => e.ClassName == "scripted_sequence" && e.GetString("m_iszEntity") == "osprey"); 20 | 21 | // Naming the script prevents it from immediately completing and removing the script itself. 22 | script?.SetTargetName("osprey_script"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities.Tests/HalfLife.UnifiedSdk.Utilities.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/RenameBlackOpsAnimationsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Renames certain animations referenced by scripted_sequences targeting monster_male_assassin 9 | /// or entities using its model to use the new animation names. 10 | /// 11 | internal sealed class RenameBlackOpsAnimationsUpgrade : MapUpgrade 12 | { 13 | private static readonly ImmutableDictionary AnimationRemap = new Dictionary 14 | { 15 | { "strafeleft", "strafeleft_cine" }, 16 | { "straferight", "straferight_cine" } 17 | }.ToImmutableDictionary(); 18 | 19 | protected override void ApplyCore(MapUpgradeContext context) 20 | { 21 | ScriptedSequenceUtilities.RenameAnimations(context, "monster_male_assassin", "models/massn.mdl", AnimationRemap); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Formats/ConverterException.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Formats 2 | { 3 | public class ConverterException : Exception 4 | { 5 | /// 6 | /// If not -1, the line number where the error was encountered. 7 | /// 8 | public int LineNumber { get; init; } = -1; 9 | 10 | /// 11 | public override string Message 12 | { 13 | get 14 | { 15 | if (LineNumber != -1) 16 | { 17 | return $"Line {LineNumber}: {base.Message}"; 18 | } 19 | 20 | return base.Message; 21 | } 22 | } 23 | 24 | /// 25 | public ConverterException() 26 | { 27 | } 28 | 29 | /// 30 | public ConverterException(string? message) 31 | : base(message) 32 | { 33 | } 34 | 35 | /// 36 | public ConverterException(string? message, Exception? innerException) 37 | : base(message, innerException) 38 | { 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/RenameConsoleCivAnimationsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 6 | { 7 | /// 8 | /// Renames certain animations referenced by scripted_sequences targeting entities using models/console_civ_scientist.mdl. 9 | /// 10 | internal sealed class RenameConsoleCivAnimationsUpgrade : MapUpgrade 11 | { 12 | private static readonly ImmutableDictionary AnimationRemap = new Dictionary 13 | { 14 | { "idle1", "console_idle1" }, 15 | { "work", "console_work" }, 16 | { "shocked", "console_shocked" }, 17 | { "sneeze", "console_sneeze" } 18 | }.ToImmutableDictionary(); 19 | 20 | protected override void ApplyCore(MapUpgradeContext context) 21 | { 22 | ScriptedSequenceUtilities.RenameAnimations(context, null, "models/console_civ_scientist.mdl", AnimationRemap); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of0a0DisableItemDroppingUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Disables item dropping for a couple NPCs in the Opposing Force intro map so players can't get weapons from them if they die. 9 | /// 10 | internal sealed class Of0a0DisableItemDroppingUpgrade : MapSpecificUpgrade 11 | { 12 | private static readonly ImmutableArray NPCTargetNames = ImmutableArray.Create( 13 | "ichabod", 14 | "booth"); 15 | 16 | public Of0a0DisableItemDroppingUpgrade() 17 | : base("of0a0") 18 | { 19 | } 20 | 21 | protected override void ApplyCore(MapUpgradeContext context) 22 | { 23 | foreach (var entity in context.Map.Entities.Where(e => NPCTargetNames.Contains(e.GetTargetName()))) 24 | { 25 | entity.SetInteger("allow_item_dropping", 0); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaOutroDisableTriggerAutoUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Sets the Remove On Fire spawnflag on the trigger_auto entity 8 | /// used to start the script on ba_outro. 9 | /// This fixes the script restarting on save load. 10 | /// 11 | internal sealed class BaOutroDisableTriggerAutoUpgrade : MapSpecificUpgrade 12 | { 13 | private const int RemoveOnFire = 1 << 0; 14 | 15 | public BaOutroDisableTriggerAutoUpgrade() 16 | : base("ba_outro") 17 | { 18 | } 19 | 20 | protected override void ApplyCore(MapUpgradeContext context) 21 | { 22 | if (context.Map.Entities 23 | .OfClass("trigger_auto") 24 | .FirstOrDefault(e => e.HasKeyValue("target", "start_outro")) is { } triggerAuto) 25 | { 26 | triggerAuto.SetSpawnFlags(triggerAuto.GetSpawnFlags() | RemoveOnFire); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapUpgrade.cs: -------------------------------------------------------------------------------- 1 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 2 | { 3 | /// 4 | /// Represents a single upgrade applied as part of an upgrade collection. 5 | /// 6 | public abstract class MapUpgrade 7 | { 8 | /// 9 | /// Applies this upgrade to the given map. 10 | /// 11 | public void Apply(MapUpgradeContext context) 12 | { 13 | if (!Filter(context)) 14 | { 15 | return; 16 | } 17 | 18 | ApplyCore(context); 19 | } 20 | 21 | /// 22 | /// Checks if this upgrade should be applied to the given map. 23 | /// 24 | /// if the upgrade should be applied, otherwise. 25 | protected virtual bool Filter(MapUpgradeContext context) 26 | { 27 | return true; 28 | } 29 | 30 | /// 31 | /// Performs the actual upgrade. 32 | /// 33 | protected abstract void ApplyCore(MapUpgradeContext context); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/Config/ManifestFilter.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Configuration; 2 | using Newtonsoft.Json; 3 | 4 | namespace HalfLife.UnifiedSdk.AssetSynchronizer.Config 5 | { 6 | internal class ManifestFilter 7 | { 8 | /// 9 | /// Path relative to the root of the assets directory to start looking for files. 10 | /// 11 | [JsonConverter(typeof(PathConverter))] 12 | public string Source { get; set; } = string.Empty; 13 | 14 | /// 15 | /// Path relative to the root of the game directory to copy files to. 16 | /// 17 | [JsonConverter(typeof(PathConverter))] 18 | public string Destination { get; set; } = string.Empty; 19 | 20 | /// 21 | /// Pattern to filter files with. Supports wildcards. 22 | /// 23 | [JsonConverter(typeof(PathConverter))] 24 | public string Pattern { get; set; } = string.Empty; 25 | 26 | /// 27 | /// Whether to recurse into subdirectories. Default . 28 | /// 29 | public bool Recursive { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ConvertBarneyModelUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 5 | { 6 | /// 7 | /// Converts monster_barney_dead entities with custom body value to use the new bodystate keyvalue. 8 | /// 9 | internal sealed class ConvertBarneyModelUpgrade : MapUpgrade 10 | { 11 | protected override void ApplyCore(MapUpgradeContext context) 12 | { 13 | foreach (var barney in context.Map.Entities.OfClass("monster_barney_dead")) 14 | { 15 | if (!barney.ContainsKey("body")) 16 | { 17 | continue; 18 | } 19 | 20 | var bodystate = barney.GetInteger("body") switch 21 | { 22 | 0 => 1, // holstered 23 | 2 => 0, // blank 24 | _ => 2 // drawn 25 | }; 26 | 27 | barney.Remove("body"); 28 | barney.SetInteger("bodystate", bodystate); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/SetCustomHullForGenericMonstersUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Sets a custom hull size for monster_generic entities that use a model that was originally hard-coded to use one. 9 | /// 10 | internal sealed class SetCustomHullForGenericMonstersUpgrade : MapUpgrade 11 | { 12 | private static readonly ImmutableArray ModelNames = ImmutableArray.Create( 13 | "models/player.mdl", 14 | "models/holo.mdl"); 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | foreach (var entity in context.Map.Entities 19 | .OfClass("monster_generic") 20 | .Where(e => ModelNames.Contains(e.GetModel()))) 21 | { 22 | entity.SetVector3("custom_hull_min", GameConstants.HullMin); 23 | entity.SetVector3("custom_hull_max", GameConstants.HullMax); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Logging/MapDiagnostics/DiagnosticsEventTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HalfLife.UnifiedSdk.Utilities.Logging.MapDiagnostics 4 | { 5 | /// Bit flags to indicate which diagnostics events to log. 6 | [Flags] 7 | public enum DiagnosticsEventTypes 8 | { 9 | /// Don't log anything. 10 | None = 0, 11 | 12 | /// Log entity created events. 13 | EntityCreated = 1 << 0, 14 | 15 | /// Log entity removed events. 16 | EntityRemoved = 1 << 1, 17 | 18 | /// Log keyvalue added events. 19 | KeyValueAdded = 1 << 2, 20 | 21 | /// Log keyvalue changed events. 22 | KeyValueChanged = 1 << 3, 23 | 24 | /// Log keyvalue removed events. 25 | KeyValueRemoved = 1 << 4, 26 | 27 | /// Log all keyvalues removed events. 28 | AllKeyValuesRemoved = 1 << 5, 29 | 30 | /// Log all events. 31 | All = EntityCreated | EntityRemoved | KeyValueAdded | KeyValueChanged | KeyValueRemoved | AllKeyValuesRemoved 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaCanal1FixAlienSlaveSpawnsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 6 | { 7 | /// 8 | /// Removes the netname keyvalue from the monstermakers that spawn in Alien Slaves 9 | /// to prevent them from waiting for 5 seconds. 10 | /// 11 | internal class BaCanal1FixAlienSlaveSpawnsUpgrade : MapSpecificUpgrade 12 | { 13 | private static readonly ImmutableArray MonsterMakerNames = ImmutableArray.Create( 14 | "tele5_spawner", 15 | "tele4_spawner"); 16 | 17 | public BaCanal1FixAlienSlaveSpawnsUpgrade() 18 | : base("ba_canal1") 19 | { 20 | } 21 | 22 | protected override void ApplyCore(MapUpgradeContext context) 23 | { 24 | foreach (var spawner in context.Map.Entities 25 | .Where(e => MonsterMakerNames.Contains(e.GetTargetName()))) 26 | { 27 | spawner.Remove("netname"); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/FixRenderColorFormatUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 5 | { 6 | /// 7 | /// Fixes the use of invalid render color formats in some maps. 8 | /// 9 | internal sealed class FixRenderColorFormatUpgrade : MapUpgrade 10 | { 11 | private static readonly Regex InvalidFormatRegex = new(@"^(\d+), (\d+), (\d+)$"); 12 | 13 | protected override void ApplyCore(MapUpgradeContext context) 14 | { 15 | foreach (var entity in context.Map.Entities) 16 | { 17 | if (entity.TryGetValue("rendercolor", out var value)) 18 | { 19 | var match = InvalidFormatRegex.Match(value); 20 | 21 | if (match.Success) 22 | { 23 | var correctColor = $"{match.Groups[1].Value} {match.Groups[2].Value} {match.Groups[3].Value}"; 24 | 25 | entity.SetString("rendercolor", correctColor); 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/Of2a2FixGruntBodyUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Fixes monster_generic entities that use hgrunt_opfor.mdl to use the correct body value. 8 | /// 9 | internal sealed class Of2a2FixGruntBodyUpgrade : MapSpecificUpgrade 10 | { 11 | public Of2a2FixGruntBodyUpgrade() 12 | : base("of2a2") 13 | { 14 | } 15 | 16 | protected override void ApplyCore(MapUpgradeContext context) 17 | { 18 | static void ChangeBody(Entity entity) 19 | { 20 | entity.SetInteger("body", 1); 21 | } 22 | 23 | var fgrunt1 = context.Map.Entities.Find("fgrunt1"); 24 | var fgrunt2 = context.Map.Entities.Find("fgrunt2"); 25 | 26 | if (fgrunt1 is not null) 27 | { 28 | ChangeBody(fgrunt1); 29 | } 30 | 31 | if (fgrunt2 is not null) 32 | { 33 | ChangeBody(fgrunt2); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapUpgradeCollection.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | using System; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 6 | { 7 | /// Represents an upgrade with events to apply changes. 8 | public sealed class MapUpgradeCollection : IComparable 9 | { 10 | private readonly ImmutableList _upgrades; 11 | 12 | /// Version this applies to. 13 | public SemVersion Version { get; } 14 | 15 | internal MapUpgradeCollection(SemVersion version, ImmutableList upgrades) 16 | { 17 | Version = version; 18 | _upgrades = upgrades; 19 | } 20 | 21 | internal void PerformUpgrade(MapUpgradeContext context) 22 | { 23 | foreach (var upgrade in _upgrades) 24 | { 25 | upgrade.Apply(context); 26 | } 27 | } 28 | 29 | /// 30 | public int CompareTo(MapUpgradeCollection? other) => Version.ComparePrecedenceTo(other?.Version); 31 | 32 | /// 33 | public override int GetHashCode() => Version.GetHashCode(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/RemapRosenbergNoUseFlagUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 5 | { 6 | /// 7 | /// Remaps monster_rosenberg's No use flag to monster_scientist's Allow follow keyvalue. 8 | /// Technically this flag is the Pre-Disaster flag used by monster_scientist 9 | /// but it is treated as a separate flag to distinguish the behavior. 10 | /// 11 | internal sealed class RemapRosenbergNoUseFlagUpgrade : MapUpgrade 12 | { 13 | private const int SF_ROSENBERG_NO_USE = 1 << 8; 14 | 15 | protected override void ApplyCore(MapUpgradeContext context) 16 | { 17 | foreach (var rosenberg in context.Map.Entities.OfClass("monster_rosenberg")) 18 | { 19 | var spawnflags = rosenberg.GetSpawnFlags(); 20 | 21 | var isSet = (spawnflags & SF_ROSENBERG_NO_USE) != 0; 22 | 23 | rosenberg.SetSpawnFlags(spawnflags & ~SF_ROSENBERG_NO_USE); 24 | 25 | rosenberg.SetInteger("allow_follow", isSet ? 0 : 1); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ReworkGamePlayerEquipUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 5 | { 6 | /// 7 | /// Changes all unnamed game_player_equip entities to be fired on player spawn. 8 | /// 9 | internal sealed class ReworkGamePlayerEquipUpgrade : MapUpgrade 10 | { 11 | private const int UseOnlyFlag = 1 << 0; 12 | 13 | protected override void ApplyCore(MapUpgradeContext context) 14 | { 15 | foreach (var entity in context.Map.Entities 16 | .OfClass("game_player_equip") 17 | .Where(e => string.IsNullOrEmpty(e.GetTargetName()))) 18 | { 19 | int spawnFlags = entity.GetSpawnFlags(); 20 | 21 | // If it's marked as use only already then it was previously unused, so skip it. 22 | if ((spawnFlags & UseOnlyFlag) != 0) 23 | { 24 | continue; 25 | } 26 | 27 | entity.SetTargetName("game_playerspawn"); 28 | 29 | entity.SetSpawnFlags(spawnFlags | UseOnlyFlag); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/AdjustBlackOpsSkinUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Adjust monster_male_assassin NPCs to use the correct head and skin value. 8 | /// 9 | internal sealed class AdjustBlackOpsSkinUpgrade : MapUpgrade 10 | { 11 | protected override void ApplyCore(MapUpgradeContext context) 12 | { 13 | foreach (var entity in context.Map.Entities.OfClass("monster_male_assassin")) 14 | { 15 | var head = entity.GetInteger("head"); 16 | 17 | int skin = 0; 18 | 19 | switch (head) 20 | { 21 | case 1: 22 | head = 0; 23 | skin = 1; 24 | break; 25 | 26 | case 2: 27 | head = 1; 28 | skin = 1; 29 | break; 30 | } 31 | 32 | entity.SetInteger("head", head); 33 | entity.SetInteger("skin", skin); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife/C2a5FixCrateGlobalNameUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.HalfLife 6 | { 7 | /// 8 | /// Removes the globalname keyvalue from the func_breakable crates next to the dam in c2a5. 9 | /// The globalname is left over from copy pasting the entity from the crates in the tunnel earlier in the map 10 | /// and causes these crates to disappear. 11 | /// 12 | internal sealed class C2a5FixCrateGlobalNameUpgrade : MapSpecificUpgrade 13 | { 14 | public C2a5FixCrateGlobalNameUpgrade() 15 | : base("c2a5") 16 | { 17 | } 18 | 19 | protected override void ApplyCore(MapUpgradeContext context) 20 | { 21 | foreach (var breakable in context.Map.Entities 22 | .Where(e => e.ClassName == "func_breakable" 23 | && e.GetTargetName() == "artillery_deploy3_expl" 24 | && e.GetGlobalName() == "c2a4g_crate4")) 25 | { 26 | breakable.Remove(KeyValueUtilities.GlobalName); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/DisableFuncTankOfPersistenceUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | using System.Collections.Immutable; 5 | 6 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 7 | { 8 | /// 9 | /// Disables the persistence behavior for all Opposing Force tank entities to match the original's behavior. 10 | /// 11 | internal sealed class DisableFuncTankOfPersistenceUpgrade : GameSpecificMapUpgrade 12 | { 13 | private static readonly ImmutableHashSet TankClassNames = ImmutableHashSet.Create( 14 | "func_tank_of", 15 | "func_tanklaser_of", 16 | "func_tankrocket_of", 17 | "func_tankmortar_of"); 18 | 19 | public DisableFuncTankOfPersistenceUpgrade() 20 | : base(ValveGames.OpposingForce) 21 | { 22 | } 23 | 24 | protected override void ApplyCore(MapUpgradeContext context) 25 | { 26 | foreach (var entity in context.Map.Entities.Where(e => TankClassNames.Contains(e.ClassName))) 27 | { 28 | entity.SetDouble("persistence", 0); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/Config/PackagePatternGroup.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Configuration; 2 | using Newtonsoft.Json; 3 | 4 | namespace HalfLife.UnifiedSdk.Packager.Config 5 | { 6 | /// 7 | /// See the MSDN documentation on the Matcher class for more information on what kind of patterns are supported: 8 | /// https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.filesystemglobbing.matcher 9 | /// 10 | internal sealed class PackagePatternGroup 11 | { 12 | [JsonProperty(Required = Required.Always)] 13 | public List Paths { get; set; } = new(); 14 | 15 | /// 16 | /// List of files and directories to package. 17 | /// Filenames ending with ".install" will be renamed to remove this extension after being added to the archive. 18 | /// 19 | [JsonProperty(Required = Required.Always, ItemConverterType = typeof(PathConverter))] 20 | public List IncludePatterns { get; set; } = new(); 21 | 22 | /// 23 | /// Files and directories to exclude from the archive. 24 | /// 25 | [JsonProperty(Required = Required.DisallowNull, ItemConverterType = typeof(PathConverter))] 26 | public List ExcludePatterns { get; set; } = new(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/IMapSerializer.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Maps; 2 | using System.IO; 3 | 4 | namespace HalfLife.UnifiedSdk.Utilities.Serialization 5 | { 6 | /// Represents a means of deserializing maps. 7 | public interface IMapSerializer 8 | { 9 | /// Which file extension this serializer applies to (including the period "."). 10 | string Extension { get; } 11 | 12 | /// Deserializes a map into a object. 13 | /// Name of the file being deserialized. 14 | /// Stream to deserialize the map from. 15 | /// 16 | /// is , contains invalid characters or the extension is not supported. 17 | /// -or- contains an empty map. 18 | /// -or- contains a map whose first entity is not worldspawn. 19 | /// -or- contains a map with more than one worldspawn. 20 | /// 21 | /// An IO error occurred during deserialization. 22 | Map Deserialize(string fileName, Stream stream); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapUpgradeContext.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | using HalfLife.UnifiedSdk.Utilities.Maps; 3 | using Semver; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 6 | { 7 | /// 8 | /// Context for map upgrade events. 9 | /// 10 | /// 11 | /// To get the version being upgraded to by the currently executing delegate, use 12 | /// 13 | /// The tool performing the upgrade. 14 | /// The version being upgraded from. 15 | /// The version being upgraded. 16 | /// 17 | /// The original version of the map, or if no version could be found. 18 | /// 19 | /// The upgrade being applied. 20 | /// The map being upgraded. 21 | /// Specifies which game the map is from. 22 | public sealed record MapUpgradeContext( 23 | MapUpgradeTool Tool, 24 | SemVersion FromVersion, 25 | SemVersion ToVersion, 26 | SemVersion OriginalVersion, 27 | MapUpgradeCollection Upgrade, 28 | Map Map, 29 | GameInfo GameInfo); 30 | } 31 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapSpecificUpgrade.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | 4 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 5 | { 6 | /// 7 | /// Helper class to apply an upgrade to a specific map. 8 | /// 9 | public abstract class MapSpecificUpgrade : MapUpgrade 10 | { 11 | /// 12 | /// The maps that this upgrade applies to. 13 | /// 14 | public ImmutableList MapNames { get; } 15 | 16 | /// 17 | /// Creates an upgrade that applies only to the specified maps. 18 | /// 19 | /// 20 | protected MapSpecificUpgrade(params string[] mapNames) 21 | { 22 | MapNames = mapNames.ToImmutableList(); 23 | } 24 | 25 | /// 26 | /// Creates an upgrade that applies only to the specified maps. 27 | /// 28 | /// 29 | protected MapSpecificUpgrade(IEnumerable mapNames) 30 | { 31 | MapNames = mapNames.ToImmutableList(); 32 | } 33 | 34 | /// 35 | protected override bool Filter(MapUpgradeContext context) 36 | { 37 | return MapNames.Contains(context.Map.BaseName); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/ConvertOtisBodyStateUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 5 | { 6 | /// 7 | /// Converts monster_otis bodystate keyvalues to no longer include the Random value, 8 | /// which is equivalent to Holstered. 9 | /// 10 | internal sealed class ConvertOtisBodyStateUpgrade : MapUpgrade 11 | { 12 | private const string BodyStateKey = "bodystate"; 13 | 14 | private enum BodyState 15 | { 16 | Random = -1, 17 | Holstered = 0, 18 | Drawn = 1 19 | } 20 | 21 | protected override void ApplyCore(MapUpgradeContext context) 22 | { 23 | foreach (var otis in context.Map.Entities.OfClass("monster_otis")) 24 | { 25 | if (!otis.ContainsKey(BodyStateKey)) 26 | { 27 | continue; 28 | } 29 | 30 | var bodystate = otis.GetInteger(BodyStateKey); 31 | 32 | if (bodystate == (int)BodyState.Random) 33 | { 34 | bodystate = (int)BodyState.Holstered; 35 | } 36 | 37 | otis.SetInteger(BodyStateKey, bodystate); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ConvertWorldItemsToItemUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Converts world_items entities to their equivalent entity. 9 | /// 10 | internal sealed class ConvertWorldItemsToItemUpgrade : MapUpgrade 11 | { 12 | private static readonly ImmutableDictionary IdToClassNameMap = new Dictionary 13 | { 14 | [42] = "item_antidote", 15 | [43] = "item_security", 16 | [44] = "item_battery", 17 | [45] = "item_suit" 18 | }.ToImmutableDictionary(); 19 | 20 | protected override void ApplyCore(MapUpgradeContext context) 21 | { 22 | foreach (var entity in context.Map.Entities.OfClass("world_items").ToList()) 23 | { 24 | var type = entity.GetInteger("type"); 25 | 26 | if (IdToClassNameMap.TryGetValue(type, out var className)) 27 | { 28 | entity.ClassName = className; 29 | entity.Remove("type"); 30 | } 31 | else 32 | { 33 | // TODO: log warning about unknown type. 34 | context.Map.Entities.Remove(entity); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeBSPFile/BSPEntity.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using System.Collections.Immutable; 3 | 4 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeBSPFile 5 | { 6 | internal sealed class BSPEntity : Entity 7 | { 8 | public Sledge.Formats.Bsp.Objects.Entity Entity { get; } 9 | 10 | public override bool IsWorldspawn { get; } 11 | 12 | public BSPEntity(EntityList entityList, Sledge.Formats.Bsp.Objects.Entity entity, bool isWorldspawn) 13 | : base(entityList, entity.SortedKeyValues.ToImmutableList()) 14 | { 15 | Entity = entity; 16 | IsWorldspawn = isWorldspawn; 17 | } 18 | 19 | protected override void SetKeyValue(int index, string key, string value, bool overwrite) 20 | { 21 | if (overwrite) 22 | { 23 | Entity.SortedKeyValues[index] = new(key, value); 24 | } 25 | else 26 | { 27 | Entity.SortedKeyValues.Insert(index, new(key, value)); 28 | } 29 | } 30 | 31 | protected override void RemoveKeyValue(int index, string key) 32 | { 33 | Entity.SortedKeyValues.RemoveAt(index); 34 | } 35 | 36 | protected override void RemoveAllKeyValues() 37 | { 38 | //Never remove the classname. 39 | var className = Entity.ClassName; 40 | Entity.SortedKeyValues.Clear(); 41 | Entity.ClassName = className; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/ChangeFuncTankOfToFuncTankUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Renames the Opposing Force func_tank classes to their original versions. 9 | /// No other changes are needed, as the original versions have been updated to include the new functionality. 10 | /// 11 | internal sealed class ChangeFuncTankOfToFuncTankUpgrade : MapUpgrade 12 | { 13 | private const string OpposingForceSuffix = "_of"; 14 | 15 | private static readonly ImmutableDictionary ClassNames = new[] 16 | { 17 | "func_tank", 18 | "func_tanklaser", 19 | "func_tankrocket", 20 | "func_tankmortar", 21 | "func_tankcontrols" 22 | } 23 | .ToImmutableDictionary(k => k + OpposingForceSuffix, v => v); 24 | 25 | protected override void ApplyCore(MapUpgradeContext context) 26 | { 27 | if (context.GameInfo != ValveGames.OpposingForce) 28 | { 29 | return; 30 | } 31 | 32 | foreach (var entity in context.Map.Entities) 33 | { 34 | if (ClassNames.TryGetValue(entity.ClassName, out var replacement)) 35 | { 36 | entity.ClassName = replacement; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapUpgradeCollectionBuilder.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | using System; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 6 | { 7 | /// 8 | /// Builds objects. 9 | /// Used as part of . 10 | /// 11 | public sealed class MapUpgradeCollectionBuilder 12 | { 13 | private readonly ImmutableList.Builder _upgrades = ImmutableList.CreateBuilder(); 14 | 15 | internal MapUpgradeCollectionBuilder() 16 | { 17 | } 18 | 19 | internal MapUpgradeCollection Build(SemVersion version) 20 | { 21 | return new MapUpgradeCollection(version, _upgrades.ToImmutable()); 22 | } 23 | 24 | /// 25 | /// Adds a new upgrade. 26 | /// 27 | /// Upgrade to add. 28 | public MapUpgradeCollectionBuilder AddUpgrade(MapUpgrade upgrade) 29 | { 30 | ArgumentNullException.ThrowIfNull(upgrade); 31 | _upgrades.Add(upgrade); 32 | return this; 33 | } 34 | 35 | /// 36 | /// Adds a new upgrade. 37 | /// 38 | /// Upgrade delegate to add. 39 | public MapUpgradeCollectionBuilder AddUpgrade(Action upgrade) 40 | { 41 | _upgrades.Add(new DelegatingMapUpgrade(upgrade)); 42 | return this; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgraderDocGenerator/HalfLife.UnifiedSdk.MapUpgraderDocGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 1.0.0 9 | SamVanheer 10 | 2024 Sam Vanheer 11 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | git 14 | README.md 15 | MIT 16 | This tool takes the XML documentation generated by the C# compiler and converts it to Markdown. 17 | MapUpgraderDocGenerator 18 | Initial public release. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | 31 | 32 | 33 | True 34 | \ 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Bsp2Obj/TextureWriter.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using SixLabors.ImageSharp; 3 | using SixLabors.ImageSharp.Formats.Tga; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using Sledge.Formats.Id; 6 | 7 | namespace HalfLife.UnifiedSdk.Bsp2Obj 8 | { 9 | internal sealed class TextureWriter 10 | { 11 | private readonly ILogger _logger; 12 | 13 | private readonly string _destinationDirectory; 14 | 15 | private readonly TgaEncoder _encoder = new() 16 | { 17 | Compression = TgaCompression.None, 18 | BitsPerPixel = TgaBitsPerPixel.Pixel24 19 | }; 20 | 21 | public TextureWriter(ILogger logger, string destinationDirectory) 22 | { 23 | _logger = logger; 24 | _destinationDirectory = destinationDirectory; 25 | } 26 | 27 | public void Write(string fileName, MipTexture texture) 28 | { 29 | var pixels = texture.MipData[0] 30 | .Select(i => new Rgb24( 31 | texture.Palette[i * 3], 32 | texture.Palette[(i * 3) + 1], 33 | texture.Palette[(i * 3) + 2])) 34 | .ToArray(); 35 | 36 | Image image = Image.LoadPixelData(pixels, (int)texture.Width, (int)texture.Height); 37 | 38 | var absoluteFileName = Path.Combine(_destinationDirectory, fileName); 39 | 40 | _logger.Information("Writing embedded texture {TextureName} to {FileName}", texture.Name, fileName); 41 | 42 | using var stream = File.Open(absoluteFileName, FileMode.Create); 43 | 44 | _encoder.Encode(image, stream); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/BaYard4aSlavesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 6 | { 7 | /// 8 | /// Fixes the Alien Slaves in ba_yard4a being resurrected by triggering them 9 | /// instead of the scripted_sequence keeping them in stasis. 10 | /// 11 | internal sealed class BaYard4aSlavesUpgrade : MapSpecificUpgrade 12 | { 13 | public BaYard4aSlavesUpgrade() 14 | : base("ba_yard4a") 15 | { 16 | } 17 | 18 | protected override void ApplyCore(MapUpgradeContext context) 19 | { 20 | ModifyScript(context, "frozen_slave_1"); 21 | ModifyScript(context, "frozen_slave_2"); 22 | } 23 | 24 | private static void ModifyScript(MapUpgradeContext context, string slaveName) 25 | { 26 | var breakable = context.Map.Entities.FirstOrDefault(e => e.ClassName == "func_breakable" && e.GetTarget() == slaveName); 27 | var script = context.Map.Entities.FirstOrDefault(e => 28 | e.ClassName == ScriptedSequenceUtilities.ClassName 29 | && e.GetString(ScriptedSequenceUtilities.TargetKey) == slaveName); 30 | 31 | if (breakable is null || script is null) 32 | { 33 | return; 34 | } 35 | 36 | var scriptName = slaveName + "_script"; 37 | 38 | script.SetTargetName(scriptName); 39 | breakable.SetTarget(scriptName); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/MonsterTentacleSpawnFlagUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Converts the Opposing Force monster_tentacle "Use Lower Model" spawnflag to instead set a custom model on the entity, 9 | /// and changes other uses to use the alternate model. 10 | /// 11 | internal sealed class MonsterTentacleSpawnFlagUpgrade : GameSpecificMapUpgrade 12 | { 13 | private const int UseLowerModel = 1 << 6; 14 | 15 | public MonsterTentacleSpawnFlagUpgrade() 16 | : base(ValveGames.OpposingForce) 17 | { 18 | } 19 | 20 | protected override void ApplyCore(MapUpgradeContext context) 21 | { 22 | foreach (var tentacle in context.Map.Entities 23 | .OfClass("monster_tentacle")) 24 | { 25 | if ((tentacle.GetSpawnFlags() & UseLowerModel) != 0) 26 | { 27 | tentacle.SetModel("models/tentacle3.mdl"); 28 | } 29 | else 30 | { 31 | tentacle.SetModel("models/tentacle2_lower.mdl"); 32 | } 33 | 34 | //Adjust height values to match Opposing Force's. 35 | tentacle.SetDouble("height0", 0); 36 | tentacle.SetDouble("height1", 136); 37 | tentacle.SetDouble("height2", 190); 38 | tentacle.SetDouble("height3", 328); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/MathUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace HalfLife.UnifiedSdk.Utilities.Tools 5 | { 6 | /// Helpers for performing math operations. 7 | public static class MathUtilities 8 | { 9 | /// 10 | /// Converts angles expressed in degrees to a set of directional unit vectors. 11 | /// 12 | /// Angles to convert. 13 | /// Forward direction. 14 | /// Right direction. 15 | /// Up direction. 16 | public static void AngleVectors(Vector3 angles, out Vector3 forward, out Vector3 right, out Vector3 up) 17 | { 18 | float angle = angles.Y * (MathF.PI * 2 / 360); 19 | float sy = MathF.Sin(angle); 20 | float cy = MathF.Cos(angle); 21 | angle = angles.X * (MathF.PI * 2 / 360); 22 | float sp = MathF.Sin(angle); 23 | float cp = MathF.Cos(angle); 24 | angle = angles.Z * (MathF.PI * 2 / 360); 25 | float sr = MathF.Sin(angle); 26 | float cr = MathF.Cos(angle); 27 | 28 | forward = new( 29 | cp * cy, 30 | cp * sy, 31 | -sp); 32 | 33 | right = new( 34 | (-1 * sr * sp * cy) + (-1 * cr * -sy), 35 | (-1 * sr * sp * sy) + (-1 * cr * cy), 36 | -1 * sr * cp); 37 | 38 | up = new( 39 | (cr * sp * cy) + (-sr * -sy), 40 | (cr * sp * sy) + (-sr * cy), 41 | cr * cp); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ConvertAngleToAnglesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Numerics; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Converts the obsolete angle keyvalue to angles. 9 | /// This is normally done by the engine, but to avoid having to account for both keyvalues in other upgrades this is done here. 10 | /// 11 | internal sealed class ConvertAngleToAnglesUpgrade : MapUpgrade 12 | { 13 | private const string AngleKey = "angle"; 14 | 15 | protected override void ApplyCore(MapUpgradeContext context) 16 | { 17 | foreach (var entity in context.Map.Entities.Where(e => e.ContainsKey(AngleKey))) 18 | { 19 | var angle = (float)entity.GetDouble(AngleKey); 20 | 21 | if (angle >= 0) 22 | { 23 | var angles = entity.GetAngles(); 24 | 25 | angles.Y = angle; 26 | 27 | entity.SetAngles(angles); 28 | } 29 | else 30 | { 31 | var angles = Vector3.Zero; 32 | 33 | if (MathF.Floor(angle) == -1) 34 | { 35 | angles.X = -90; 36 | } 37 | else 38 | { 39 | angles.X = 90; 40 | } 41 | 42 | entity.SetAngles(angles); 43 | } 44 | 45 | entity.Remove(AngleKey); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/MapUpgradeToolFactory.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 2 | using Semver; 3 | using Serilog; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades 6 | { 7 | /// 8 | /// Factory for creating an upgrade tool. 9 | /// 10 | public static class MapUpgradeToolFactory 11 | { 12 | /// 13 | /// Creates an upgrade tool that applies the upgrades needed to upgrade a map to the latest version of the Unified SDK. 14 | /// 15 | public static MapUpgradeTool Create(ILogger logger, DiagnosticsLevel diagnosticsLevel) 16 | { 17 | return MapUpgradeToolBuilder.Build(builder => 18 | { 19 | builder.AddUpgrades(new SemVersion(1, 0, 0), upgrade => 20 | { 21 | upgrade 22 | .AddSharedUpgrades() 23 | .AddHalfLifeUpgrades() 24 | .AddOpposingForceUpgrades() 25 | .AddBlueShiftUpgrades(); 26 | }); 27 | 28 | builder.WithDiagnostics(logger, diagnosticsBuilder => 29 | { 30 | if (diagnosticsLevel != DiagnosticsLevel.Disabled) 31 | { 32 | diagnosticsBuilder.WithAllEventTypes(); 33 | 34 | if (diagnosticsLevel != DiagnosticsLevel.All) 35 | { 36 | diagnosticsBuilder.IgnoreKeys("angle", "angles", MapUpgradeTool.DefaultGameVersionKey); 37 | } 38 | } 39 | }); 40 | }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.KeyValueMatcher/HalfLife.UnifiedSdk.KeyValueMatcher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 1.0.0 9 | SamVanheer 10 | 2024 Sam Vanheer 11 | Searches through Half-Life 1 BSP files in a given directory, printing entities that match the given regular expression patterns 12 | README.md 13 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | halflife;half-life;valve 16 | git 17 | Initial public release. 18 | MIT 19 | KeyValueMatcher 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader/HalfLife.UnifiedSdk.MapUpgrader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 1.0.0 9 | SamVanheer 10 | 2024 Sam Vanheer 11 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | git 14 | README.md 15 | MIT 16 | This tool upgrades Half-Life maps to the latest version of the Unified SDK. 17 | MapUpgrader 18 | Initial public release. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | 31 | 32 | 33 | True 34 | \ 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapCfgGenerator/HalfLife.UnifiedSdk.MapCfgGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | SamVanheer 9 | 1.0.0 10 | This program generates map configurations for official Valve maps. 11 | 2024 Sam Vanheer 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | git 16 | halflife;half-life;valve;json 17 | MIT 18 | Initial public release. 19 | MapCfgGenerator 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities.Tests/EntityTests.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Maps; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using System; 4 | using Xunit; 5 | 6 | namespace HalfLife.UnifiedSdk.Utilities.Tests 7 | { 8 | public class EntityTests 9 | { 10 | private static Map CreateEmptyMap() 11 | { 12 | return MapFormats.CreateEntMap("EmptyMap"); 13 | } 14 | 15 | [Fact] 16 | public void Worldspawn_ChangingClassNameThroughProperty_Throws() 17 | { 18 | var map = CreateEmptyMap(); 19 | 20 | var worldspawn = map.Entities.Worldspawn; 21 | 22 | Assert.Throws(() => worldspawn.ClassName = "foo"); 23 | } 24 | 25 | [Fact] 26 | public void Worldspawn_ChangingClassNameThroughIndexer_Throws() 27 | { 28 | var map = CreateEmptyMap(); 29 | 30 | var worldspawn = map.Entities.Worldspawn; 31 | 32 | Assert.Throws(() => worldspawn[KeyValueUtilities.ClassName] = "foo"); 33 | } 34 | 35 | [Fact] 36 | public void Worldspawn_ChangingClassNameThroughSetString_Throws() 37 | { 38 | var map = CreateEmptyMap(); 39 | 40 | var worldspawn = map.Entities.Worldspawn; 41 | 42 | Assert.Throws(() => worldspawn.SetString(KeyValueUtilities.ClassName, "foo")); 43 | } 44 | 45 | [Fact] 46 | public void NotWorldspawn_ChangingClassNameToWorldspawn_Throws() 47 | { 48 | var map = CreateEmptyMap(); 49 | 50 | var entity = map.Entities.CreateNewEntity("foo"); 51 | 52 | Assert.Throws(() => entity.ClassName = KeyValueUtilities.WorldspawnClassName); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Hud2Json/HalfLife.UnifiedSdk.Hud2Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | SamVanheer 9 | 1.0.0 10 | This program converts original Half-Life hud.txt and weapon_*.txt files to the Unified SDK's hud.json format. 11 | 2024 Sam Vanheer 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | git 16 | halflife;half-life;valve;json 17 | MIT 18 | Initial public release. 19 | Hud2Json 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Skill2Json/HalfLife.UnifiedSdk.Skill2Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | SamVanheer 9 | 1.0.0 10 | This program converts original Half-Life skill.cfg files to the Unified SDK's skill.json format. 11 | 2024 Sam Vanheer 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | git 16 | halflife;half-life;valve;json 17 | MIT 18 | Initial public release. 19 | Skill2Json 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.ContentInstaller/HalfLife.UnifiedSdk.ContentInstaller.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 9 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 10 | 2024 Sam Vanheer 11 | 1.0.0 12 | SamVanheer 13 | README.md 14 | git 15 | halflife;half-life;valve 16 | MIT 17 | Initial public release. 18 | This tool copies, converts and upgrades Half-Life game assets for use in a mod. 19 | ContentInstaller 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Materials2Json/HalfLife.UnifiedSdk.Materials2Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | SamVanheer 9 | 1.0.0 10 | This program converts original Half-Life materials.txt files to the Unified SDK's materials.json format. 11 | 2024 Sam Vanheer 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | git 16 | halflife;half-life;valve;json 17 | MIT 18 | Initial public release. 19 | Materials2Json 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Sentences2Json/HalfLife.UnifiedSdk.Sentences2Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | SamVanheer 9 | 1.0.0 10 | This program converts original Half-Life sentences.txt files to the Unified SDK's sentences.json format. 11 | 2024 Sam Vanheer 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | git 16 | halflife;half-life;valve;json 17 | MIT 18 | Initial public release. 19 | Sentences2Json 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/HalfLife.UnifiedSdk.AssetSynchronizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 9 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 10 | 2024 Sam Vanheer 11 | 1.0.0 12 | SamVanheer 13 | README.md 14 | git 15 | halflife;half-life;valve;json 16 | MIT 17 | Initial public release. 18 | This tool copies a set of directories to a target directory and monitors for changes while running. 19 | AssetSynchronizer 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ConvertWorldspawnGameTitleValueUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Converts the gametitle keyvalue to a string containing the game name. 9 | /// 10 | internal sealed class ConvertWorldspawnGameTitleValueUpgrade : MapUpgrade 11 | { 12 | private const string GameTitleKey = "gametitle"; 13 | 14 | protected override void ApplyCore(MapUpgradeContext context) 15 | { 16 | var worldspawn = context.Map.Entities.Worldspawn; 17 | 18 | //Although the game uses a spawnflag to track this internally, 19 | //since it's undocumented and not used in official games it's not checked here. 20 | if (worldspawn.ContainsKey(GameTitleKey)) 21 | { 22 | var value = worldspawn.GetInteger(GameTitleKey); 23 | 24 | if (value != 0) 25 | { 26 | var titleToUse = context.Map.BaseName switch 27 | { 28 | "of0a0" => ValveGames.OpposingForce.ModDirectory, 29 | "ba_tram1" => ValveGames.BlueShift.ModDirectory, 30 | //For c0a0, custom maps, and anything else. 31 | _ => ValveGames.HalfLife1.ModDirectory 32 | }; 33 | 34 | worldspawn.SetString(GameTitleKey, titleToUse); 35 | } 36 | else 37 | { 38 | //Disabled; remove the key. 39 | worldspawn.Remove(GameTitleKey); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/HalfLife.UnifiedSdk.Packager.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 1.0.0 9 | SamVanheer 10 | 2024 Sam Vanheer 11 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | git 15 | halflife;half-life;valve;json 16 | MIT 17 | Initial public release. 18 | This tool packages Half-Life mods into a zip archive. 19 | Packager 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | True 35 | \ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities.Tests/MapFormatsTests.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using System.IO; 3 | using System.Text; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace HalfLife.UnifiedSdk.Utilities.Tests 8 | { 9 | public class MapFormatsTests 10 | { 11 | private readonly ITestOutputHelper _output; 12 | 13 | public MapFormatsTests(ITestOutputHelper output) 14 | { 15 | _output = output; 16 | } 17 | 18 | [Fact] 19 | public void EmptyMapString_CreatesEmptyMap() 20 | { 21 | var stream = new MemoryStream(Encoding.UTF8.GetBytes(KeyValueUtilities.EmptyMapString)); 22 | 23 | var map = MapFormats.Deserialize("test.ent", stream); 24 | 25 | Assert.Single(map.Entities); 26 | 27 | Assert.Equal("worldspawn", map.Entities.Worldspawn.ClassName); 28 | Assert.Equal("worldspawn", map.Entities[0].ClassName); 29 | } 30 | 31 | [Fact] 32 | public void EmptyMapString_WritesEmptyMap() 33 | { 34 | var emptyMapBytes = Encoding.UTF8.GetBytes(KeyValueUtilities.EmptyMapString); 35 | 36 | var stream = new MemoryStream(emptyMapBytes); 37 | 38 | var map = MapFormats.Deserialize("test.ent", stream); 39 | 40 | using var destination = new MemoryStream(); 41 | 42 | map.Serialize(destination); 43 | 44 | var resultText = Encoding.UTF8.GetString(destination.ToArray()); 45 | 46 | _output.WriteLine(resultText); 47 | 48 | //EmptyMapString uses line endings based on the platform the code was compiled on 49 | //while the writer always uses \n, so ignore the differences. 50 | Assert.Equal(KeyValueUtilities.EmptyMapString + '\0', resultText, ignoreLineEndingDifferences: true); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/ChangeRosenbergModelUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 6 | { 7 | internal sealed class ChangeRosenbergModelUpgrade : GameSpecificMapUpgrade 8 | { 9 | public ChangeRosenbergModelUpgrade() 10 | : base(ValveGames.BlueShift) 11 | { 12 | } 13 | 14 | protected override void ApplyCore(MapUpgradeContext context) 15 | { 16 | foreach (var entity in context.Map.Entities 17 | .Where(e => e.ClassName == "monster_rosenberg" 18 | || (e.ClassName == "monster_generic" 19 | && e.GetModel() == "models/scientist.mdl" 20 | && e.GetInteger("body") == 3))) 21 | { 22 | UpdateEntity(entity); 23 | } 24 | 25 | // Remap Rosenberg monster_scientist to monster_rosenberg. 26 | foreach (var entity in context.Map.Entities 27 | .Where(e => e.ClassName == "monster_scientist" 28 | && e.GetInteger("body") == 3)) 29 | { 30 | entity.ClassName = "monster_rosenberg"; 31 | UpdateEntity(entity); 32 | } 33 | } 34 | private static void UpdateEntity(Entity entity) 35 | { 36 | // The default model works fine for monster_rosenberg 37 | if (entity.ClassName == "monster_rosenberg") 38 | { 39 | entity.Remove("model"); 40 | } 41 | else 42 | { 43 | entity.SetModel("models/rosenberg.mdl"); 44 | } 45 | 46 | entity.Remove("body"); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Bsp2Obj/HalfLife.UnifiedSdk.Bsp2Obj.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | SamVanheer 9 | 1.0.0 10 | This program converts Half-Life 1 BSP files to the Wavefront OBJ format. 11 | 2024 Sam Vanheer 12 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 13 | README.md 14 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 15 | git 16 | halflife;half-life;valve;json 17 | MIT 18 | Initial public release. 19 | Bsp2Obj 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/FixNonLoopingSoundsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Fixes ambient_generic entities using non-looping sounds 9 | /// to stop them from restarting when loading a save game. 10 | /// 11 | internal sealed class FixNonLoopingSoundsUpgrade : MapUpgrade 12 | { 13 | private const string MessageKey = "message"; 14 | 15 | private static readonly ImmutableHashSet MapSoundFileNames = ImmutableHashSet.Create( 16 | "ambience/alienflyby1.wav", 17 | "misc/ear_ringing.wav", 18 | "weapons/explode3.wav", 19 | "weapons/explode4.wav", 20 | "weapons/mortar.wav", 21 | "nihilanth/nil_alone.wav", 22 | "nihilanth/nil_win.wav", 23 | "Gonarch/gon_die1.wav"); 24 | 25 | private enum AmbientGenericSpawnFlags 26 | { 27 | NotToggled = 1 << 5 28 | } 29 | 30 | protected override void ApplyCore(MapUpgradeContext context) 31 | { 32 | foreach (var entity in context.Map.Entities.OfClass("ambient_generic")) 33 | { 34 | var soundFileName = entity.GetString(MessageKey); 35 | 36 | if (!MapSoundFileNames.Contains(soundFileName)) 37 | { 38 | continue; 39 | } 40 | 41 | int flags = entity.GetSpawnFlags(); 42 | 43 | if ((flags & (int)AmbientGenericSpawnFlags.NotToggled) == 0) 44 | { 45 | flags |= (int)AmbientGenericSpawnFlags.NotToggled; 46 | 47 | entity.SetSpawnFlags(flags); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/HalfLife.UnifiedSdk.Utilities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | True 7 | 1.0.0 8 | SamVanheer 9 | Contains utility functionality for opening, analyzing, modifying, converting and upgrading Half-Life 1 maps made for the GoldSource engine. 10 | 2024 Sam Vanheer 11 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 12 | README.md 13 | https://github.com/SamVanheer/HalfLife.UnifiedSdk-CSharp 14 | git 15 | halflife;half-life;valve;bsp 16 | Initial public release. 17 | MIT 18 | True 19 | 20 | 21 | 22 | 23 | True 24 | \ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Maps/Map.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using System; 3 | using System.IO; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Maps 6 | { 7 | /// Provides access to map data. 8 | public abstract class Map 9 | { 10 | /// File name of this map. 11 | public string FileName { get; } 12 | 13 | /// 14 | /// Base name of the map, used in trigger_changelevel and the changelevel command among other things. 15 | /// 16 | public string BaseName => Path.GetFileNameWithoutExtension(FileName); 17 | 18 | /// Which content type the map data is in. 19 | /// 20 | public MapContentType ContentType { get; } 21 | 22 | /// Whether this map is a compiled or in a source content type. 23 | /// 24 | public bool IsCompiled => ContentType == MapContentType.Compiled; 25 | 26 | private EntityList? _entityList; 27 | 28 | /// List of entities in the map. 29 | public EntityList Entities => _entityList ??= CreateEntities(); 30 | 31 | /// Creates a map data object. 32 | /// If is . 33 | protected Map(string fileName, MapContentType contentType) 34 | { 35 | FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); 36 | ContentType = contentType; 37 | } 38 | 39 | /// Creates an entity list from this map's data. The entity list can modify the map's entities. 40 | protected abstract EntityList CreateEntities(); 41 | 42 | /// Serializes this map to the given stream. 43 | public abstract void Serialize(Stream stream); 44 | 45 | /// 46 | public override string ToString() => BaseName; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/PruneExcessMultiManagerKeysUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | using System.Collections.Immutable; 5 | 6 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 7 | { 8 | /// 9 | /// Prunes excess keyvalues specified for multi_manager entities. 10 | /// In practice this only affects a handful of entities used in retinal scanner scripts. 11 | /// 12 | internal sealed class PruneExcessMultiManagerKeysUpgrade : MapUpgrade 13 | { 14 | /// 15 | /// Original HL1 SDK limit. 16 | /// 17 | private const int MaxKeys = 16; 18 | 19 | /// 20 | /// These keys are not counted as targets by multi_manager. 21 | /// There are more of these but normally they aren't used. 22 | /// 23 | private static readonly ImmutableHashSet KeysToIgnore = ImmutableHashSet.Create( 24 | KeyValueUtilities.ClassName, 25 | KeyValueUtilities.TargetName, 26 | KeyValueUtilities.Origin, 27 | "wait"); 28 | 29 | protected override void ApplyCore(MapUpgradeContext context) 30 | { 31 | foreach (var multiManager in context.Map.Entities.OfClass("multi_manager")) 32 | { 33 | var keys = multiManager 34 | .Select((kv, index) => new { KeyValue = kv, Index = index }) 35 | .Where(kv => !KeysToIgnore.Contains(kv.KeyValue.Key)) 36 | .ToList(); 37 | 38 | if (keys.Count > MaxKeys) 39 | { 40 | // Need to remove last to first to avoid invalidating indices! 41 | foreach (var key in keys.Skip(MaxKeys).Reverse()) 42 | { 43 | multiManager.RemoveAt(key.Index); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/UpgradeTool/MapUpgradeCommand.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | using HalfLife.UnifiedSdk.Utilities.Maps; 3 | using Semver; 4 | using System; 5 | 6 | namespace HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool 7 | { 8 | /// A map upgrade command. 9 | public sealed class MapUpgradeCommand 10 | { 11 | /// Map being upgraded. 12 | public Map Map { get; } 13 | 14 | /// 15 | /// Version to upgrade from. If left as , the map will be upgraded from its current version. 16 | /// If no current version key can be found in the map, the map will be upgraded from the first known version. 17 | /// 18 | public SemVersion? From { get; init; } 19 | 20 | /// 21 | /// Version to upgrade to. If left as , the map will be upgraded to the latest version. 22 | /// 23 | /// 24 | public SemVersion? To { get; init; } 25 | 26 | /// 27 | /// If is older than the version set by the map, throw an exception. 28 | /// Default true. This protects against upgrading maps that are already upgraded, which could break entity setups. 29 | /// 30 | public bool ThrowOnTooOldVersion { get; init; } = true; 31 | 32 | /// 33 | /// Specifies which game the map is from. 34 | /// If the map did not come from a game installation, use one of the generic game info objects. 35 | /// 36 | public GameInfo GameInfo { get; } 37 | 38 | /// Creates a new map upgrde command. 39 | /// is null. 40 | public MapUpgradeCommand(Map map, GameInfo gameInfo) 41 | { 42 | Map = map ?? throw new ArgumentNullException(nameof(map)); 43 | GameInfo = gameInfo ?? throw new ArgumentNullException(nameof(gameInfo)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/FileCopier.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.Collections.Concurrent; 3 | 4 | namespace HalfLife.UnifiedSdk.AssetSynchronizer 5 | { 6 | /// 7 | /// Runs file copy operations on a separate thread to minimize time spent responding to FileSystemWatcher events. 8 | /// 9 | internal sealed class FileCopier 10 | { 11 | private readonly ILogger _logger; 12 | 13 | private readonly CancellationToken _cancellationToken; 14 | 15 | private readonly ConcurrentQueue _queue = new(); 16 | 17 | public FileCopier(ILogger logger, CancellationToken cancellationToken) 18 | { 19 | _logger = logger; 20 | _cancellationToken = cancellationToken; 21 | } 22 | 23 | public void Run() 24 | { 25 | while (!_cancellationToken.IsCancellationRequested) 26 | { 27 | while (_queue.TryDequeue(out var item)) 28 | { 29 | CopyFile(item); 30 | } 31 | } 32 | } 33 | 34 | public void Add(FileCopyItem item) 35 | { 36 | _queue.Enqueue(item); 37 | } 38 | 39 | private void CopyFile(FileCopyItem item) 40 | { 41 | //Rebase the filename to the destination. 42 | var relativePath = Path.GetRelativePath(item.Watcher.SourcePath, item.FileName); 43 | var destinationFileName = Path.Combine(item.Watcher.DestinationPath, relativePath); 44 | 45 | try 46 | { 47 | Directory.CreateDirectory(item.Watcher.DestinationPath); 48 | File.Copy(item.FileName, destinationFileName, true); 49 | } 50 | catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) 51 | { 52 | //If anything goes wrong the user should know about it so they can correct the problem. 53 | //E.g. trying to copy a file but a directory with the same name exists. 54 | WatcherHelpers.PrintException(_logger, e); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Hud2Json/Program.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Formats.Hud; 2 | using HalfLife.UnifiedSdk.Utilities.Logging; 3 | using System.CommandLine; 4 | using System.CommandLine.Parsing; 5 | 6 | namespace HalfLife.UnifiedSdk.Hud2Json 7 | { 8 | internal static class Program 9 | { 10 | const string TargetExtension = ".json"; 11 | 12 | public static async Task Main(string[] args) 13 | { 14 | var inputFileName = new Argument("filename", "hud definition to convert"); 15 | 16 | var outputFileName = new Option("--output-filename", 17 | getDefaultValue: () => null, 18 | "If provided, the name of the file to write the hud.json contents to.\n" + 19 | "Otherwise the file is saved to the source directory with the same name and 'json' extension."); 20 | 21 | var rootCommand = new RootCommand("Half-Life Unified SDK hud.txt to hud.json converter") 22 | { 23 | inputFileName, 24 | outputFileName 25 | }; 26 | 27 | rootCommand.SetHandler((fileName, outputFileName, logger) => 28 | { 29 | if (fileName.Extension == TargetExtension) 30 | { 31 | throw new ArgumentException( 32 | $"Input file \"{fileName.FullName}\" has the same extension as the target file type", nameof(fileName)); 33 | } 34 | 35 | //Default to input with different extension. 36 | outputFileName ??= new FileInfo(Path.ChangeExtension(fileName.FullName, TargetExtension)); 37 | 38 | logger.Information("Writing output to \"{FileName}\"", outputFileName.FullName); 39 | 40 | using var inputStream = fileName.OpenRead(); 41 | using var outputStream = outputFileName.Open(FileMode.Create); 42 | 43 | HudConverter.Convert(inputStream, outputStream, logger); 44 | }, 45 | inputFileName, outputFileName, LoggerBinder.Instance); 46 | 47 | return await rootCommand.InvokeAsync(args); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/RenameIntroGruntAnimationsUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Renames the intro grunt animations. 9 | /// 10 | internal sealed class RenameIntroGruntAnimationsUpgrade : MapUpgrade 11 | { 12 | private record GruntData(string ModelName, ImmutableDictionary AnimationRemap); 13 | 14 | private static readonly ImmutableArray GruntDatas = ImmutableArray.Create( 15 | new GruntData("models/intro_commander.mdl", new Dictionary 16 | { 17 | { "fall_out", "intro_stand_fallout" }, 18 | { "stand_react", "intro_stand_react" }, 19 | { "bark", "intro_stand_bark" } 20 | }.ToImmutableDictionary()), 21 | new GruntData("models/intro_medic.mdl", Enumerable.Range(1, 7).ToImmutableDictionary(s => "sitting" + s, s => "intro_sitting_holster" + s)), 22 | new GruntData("models/intro_regular.mdl", Enumerable.Range(1, 5).ToImmutableDictionary(s => "sitting" + s, s => "intro_sitting_mp5_" + s)), 23 | new GruntData("models/intro_saw.mdl", Enumerable.Range(1, 5).ToImmutableDictionary(s => "sitting" + s, s => "intro_sitting_mp5_" + s) 24 | .Add("stiff", "intro_sitting_stiff") 25 | .Add("sit_React", "intro_sitting_react") 26 | .Add("cower", "intro_sitting_cower1")), 27 | new GruntData("models/intro_torch.mdl", Enumerable.Range(1, 7).ToImmutableDictionary(s => "sitting" + s, s => "intro_sitting_holster" + s) 28 | .Add("cower", "intro_sitting_cower2")) 29 | ); 30 | 31 | protected override void ApplyCore(MapUpgradeContext context) 32 | { 33 | foreach (var data in GruntDatas) 34 | { 35 | ScriptedSequenceUtilities.RenameAnimations(context, null, data.ModelName, data.AnimationRemap); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Materials2Json/Program.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Formats.Materials; 2 | using HalfLife.UnifiedSdk.Utilities.Logging; 3 | using System.CommandLine; 4 | using System.CommandLine.Parsing; 5 | 6 | namespace HalfLife.UnifiedSdk.Materials2Json 7 | { 8 | internal static class Program 9 | { 10 | const string TargetExtension = ".json"; 11 | 12 | public static async Task Main(string[] args) 13 | { 14 | var inputFileName = new Argument("filename", "materials to convert"); 15 | 16 | var outputFileName = new Option("--output-filename", 17 | getDefaultValue: () => null, 18 | "If provided, the name of the file to write the materials.json contents to.\n" + 19 | "Otherwise the file is saved to the source directory with the same name and 'json' extension."); 20 | 21 | var rootCommand = new RootCommand("Half-Life Unified SDK materials.txt to materials.json converter") 22 | { 23 | inputFileName, 24 | outputFileName 25 | }; 26 | 27 | rootCommand.SetHandler((fileName, outputFileName, logger) => 28 | { 29 | if (fileName.Extension == TargetExtension) 30 | { 31 | throw new ArgumentException( 32 | $"Input file \"{fileName.FullName}\" has the same extension as the target file type", nameof(fileName)); 33 | } 34 | 35 | //Default to input with different extension. 36 | outputFileName ??= new FileInfo(Path.ChangeExtension(fileName.FullName, TargetExtension)); 37 | 38 | logger.Information("Writing output to \"{FileName}\"", outputFileName.FullName); 39 | 40 | using var inputStream = fileName.OpenRead(); 41 | using var outputStream = outputFileName.Open(FileMode.Create); 42 | 43 | MaterialsConverter.Convert(inputStream, outputStream, logger); 44 | }, 45 | inputFileName, outputFileName, LoggerBinder.Instance); 46 | 47 | return await rootCommand.InvokeAsync(args); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Sentences2Json/Program.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Formats.Sentences; 2 | using HalfLife.UnifiedSdk.Utilities.Logging; 3 | using System.CommandLine; 4 | using System.CommandLine.Parsing; 5 | 6 | namespace HalfLife.UnifiedSdk.Sentences2Json 7 | { 8 | internal static class Program 9 | { 10 | const string TargetExtension = ".json"; 11 | 12 | public static async Task Main(string[] args) 13 | { 14 | var inputFileName = new Argument("filename", "Sentences definition to convert"); 15 | 16 | var outputFileName = new Option("--output-filename", 17 | getDefaultValue: () => null, 18 | "If provided, the name of the file to write the sentences.json contents to.\n" + 19 | "Otherwise the file is saved to the source directory with the same name and 'json' extension."); 20 | 21 | var rootCommand = new RootCommand("Half-Life Unified SDK sentences.txt to sentences.json converter") 22 | { 23 | inputFileName, 24 | outputFileName 25 | }; 26 | 27 | rootCommand.SetHandler((fileName, outputFileName, logger) => 28 | { 29 | if (fileName.Extension == TargetExtension) 30 | { 31 | throw new ArgumentException( 32 | $"Input file \"{fileName.FullName}\" has the same extension as the target file type", nameof(fileName)); 33 | } 34 | 35 | //Default to input with different extension. 36 | outputFileName ??= new FileInfo(Path.ChangeExtension(fileName.FullName, TargetExtension)); 37 | 38 | logger.Information("Writing output to \"{FileName}\"", outputFileName.FullName); 39 | 40 | using var inputStream = fileName.OpenRead(); 41 | using var outputStream = outputFileName.Open(FileMode.Create); 42 | 43 | SentencesConverter.Convert(inputStream, outputStream, logger); 44 | }, 45 | inputFileName, outputFileName, LoggerBinder.Instance); 46 | 47 | return await rootCommand.InvokeAsync(args); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/ConvertBreakableItemUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Converts func_breakable's spawn object keyvalue from an index to a classname. 9 | /// 10 | internal sealed class ConvertBreakableItemUpgrade : MapUpgrade 11 | { 12 | private const string ItemKey = "spawnobject"; 13 | 14 | private static readonly ImmutableList ClassNames = ImmutableList.Create( 15 | "item_battery", 16 | "item_healthkit", 17 | "weapon_9mmhandgun", 18 | "ammo_9mmclip", 19 | "weapon_9mmar", 20 | "ammo_9mmar", 21 | "ammo_argrenades", 22 | "weapon_shotgun", 23 | "ammo_buckshot", 24 | "weapon_crossbow", 25 | "ammo_crossbow", 26 | "weapon_357", 27 | "ammo_357", 28 | "weapon_rpg", 29 | "ammo_rpgclip", 30 | "ammo_gaussclip", 31 | "weapon_handgrenade", 32 | "weapon_tripmine", 33 | "weapon_satchel", 34 | "weapon_snark", 35 | "weapon_hornetgun", 36 | "weapon_penguin" 37 | ); 38 | 39 | protected override void ApplyCore(MapUpgradeContext context) 40 | { 41 | foreach (var entity in context.Map.Entities 42 | .Where(e => e.ClassName == "func_breakable" || e.ClassName == "func_pushable")) 43 | { 44 | int index = entity.GetInteger(ItemKey, 0); 45 | 46 | if (index == 0) 47 | { 48 | entity.Remove(ItemKey); 49 | } 50 | else if (index > 0 && index <= ClassNames.Count) 51 | { 52 | entity.SetString(ItemKey, ClassNames[index - 1]); 53 | } 54 | else 55 | { 56 | // TODO: log error 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/SentenceUtilities.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | 6 | namespace HalfLife.UnifiedSdk.Utilities.Tools 7 | { 8 | /// Helper functions for dealing with sentences. 9 | public static class SentenceUtilities 10 | { 11 | private static readonly ImmutableDictionary EntityKeyNames = new Dictionary 12 | { 13 | { "scripted_sentence", "sentence" }, 14 | { "ambient_generic", "message" } 15 | }.ToImmutableDictionary(); 16 | 17 | /// Remaps sentences listed in . 18 | public static void ReplaceSentences(IEnumerable entities, ImmutableDictionary replacementMap) 19 | { 20 | ArgumentNullException.ThrowIfNull(entities); 21 | ArgumentNullException.ThrowIfNull(replacementMap); 22 | 23 | foreach (var entity in entities) 24 | { 25 | if (EntityKeyNames.TryGetValue(entity.ClassName, out var key)) 26 | { 27 | ReplaceSentencesCore(entity, key, replacementMap); 28 | } 29 | } 30 | } 31 | 32 | /// Remaps sentences listed in . 33 | public static void ReplaceSentences(Entity entity, string key, ImmutableDictionary replacementMap) 34 | { 35 | ArgumentNullException.ThrowIfNull(entity); 36 | ArgumentNullException.ThrowIfNull(key); 37 | ArgumentNullException.ThrowIfNull(replacementMap); 38 | 39 | ReplaceSentencesCore(entity, key, replacementMap); 40 | } 41 | 42 | private static void ReplaceSentencesCore(Entity entity, string key, ImmutableDictionary replacementMap) 43 | { 44 | if (entity.TryGetValue(key, out var name) && replacementMap.TryGetValue(name, out var replacement)) 45 | { 46 | entity.SetString(key, replacement); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/RenameMessagesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 7 | { 8 | /// 9 | /// Renames the messages used in env_message entities and worldspawn to use a game-specific prefix. 10 | /// 11 | internal sealed class RenameMessagesUpgrade : MapUpgrade 12 | { 13 | private const string MessageKey = "message"; 14 | private const string WorldspawnKey = "chaptertitle"; 15 | 16 | private static readonly ImmutableArray ClassNames = ImmutableArray.Create( 17 | "env_message", 18 | "player_loadsaved" 19 | ); 20 | 21 | private static readonly ImmutableList Patterns = ImmutableList.Create( 22 | new Regex(@"^CR\d+$"), 23 | new Regex(@"^END\d+$"), 24 | new Regex("^GAMEOVER$"), 25 | new Regex("^TRAITOR$"), 26 | new Regex("^LOSER$"), 27 | new Regex("^GAMETITLE$"), 28 | new Regex("^T0A0TITLE$"), 29 | new Regex("^HZBUTTON1$"), 30 | new Regex("^HZBARNEY$") 31 | ); 32 | 33 | protected override void ApplyCore(MapUpgradeContext context) 34 | { 35 | var prefix = context.GameInfo.ModDirectory.ToUpperInvariant() + '_'; 36 | 37 | void CheckMessage(Entity entity, string key) 38 | { 39 | if (entity.TryGetValue(key, out var message) 40 | && Patterns.Any(p => p.IsMatch(message))) 41 | { 42 | entity.SetString(key, prefix + message); 43 | } 44 | } 45 | 46 | foreach (var entity in context.Map.Entities.Where(e => ClassNames.Contains(e.ClassName))) 47 | { 48 | CheckMessage(entity, MessageKey); 49 | } 50 | 51 | // Worldspawn creates an env_message to handle this, so make sure it also gets converted. 52 | CheckMessage(context.Map.Entities.Worldspawn, WorldspawnKey); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/Ripent.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Tools 6 | { 7 | /// Provides Ripent functionality. 8 | public static class Ripent 9 | { 10 | /// Creates a .ent file from a BSP file with the given name. 11 | public static void Export(string bspFileName, string entFileName) 12 | { 13 | var bspMap = MapFormats.Deserialize(bspFileName); 14 | 15 | //Create an empty .ent map. 16 | var entMap = MapFormats.Ent.Deserialize(entFileName, new MemoryStream(Encoding.UTF8.GetBytes(KeyValueUtilities.EmptyMapString))); 17 | 18 | entMap.Entities.ReplaceWith(bspMap.Entities); 19 | 20 | using var stream = File.Open(entFileName, FileMode.Create, FileAccess.Write); 21 | 22 | entMap.Serialize(stream); 23 | } 24 | 25 | /// Creates a .ent file from a BSP file with the same name. 26 | public static void Export(string bspFileName) 27 | { 28 | var entFileName = Path.ChangeExtension(bspFileName, MapFormats.Ent.Extension); 29 | Export(bspFileName, entFileName); 30 | } 31 | 32 | /// Overwrites the given BSP file's entity data with a .ent file with the given name. 33 | public static void Import(string bspFileName, string entFileName) 34 | { 35 | //Always deserialize as bsp. 36 | var bspMap = MapFormats.Deserialize(bspFileName, MapFormats.Bsp); 37 | var entMap = MapFormats.Deserialize(entFileName, MapFormats.Ent); 38 | 39 | bspMap.Entities.ReplaceWith(entMap.Entities); 40 | 41 | using var stream = File.Open(bspFileName, FileMode.Create, FileAccess.Write); 42 | 43 | bspMap.Serialize(stream); 44 | } 45 | 46 | /// Overwrites the given BSP file's entity data with a .ent file with the same name. 47 | public static void Import(string bspFileName) 48 | { 49 | var entFileName = Path.ChangeExtension(bspFileName, MapFormats.Ent.Extension); 50 | Import(bspFileName, entFileName); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/Watcher.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | 3 | namespace HalfLife.UnifiedSdk.AssetSynchronizer 4 | { 5 | /// 6 | /// Watches for file changes, creations and renaming and mirrors it to the destination. 7 | /// 8 | internal sealed class Watcher : IDisposable 9 | { 10 | private readonly ILogger _logger; 11 | 12 | private readonly FileCopier _fileCopier; 13 | 14 | private readonly FileSystemWatcher _watcher; 15 | 16 | public string SourcePath => _watcher.Path; 17 | 18 | public string DestinationPath { get; } 19 | 20 | public Watcher(ILogger logger, FileCopier fileCopier, string source, string destination, string pattern, bool recursive) 21 | { 22 | _logger = logger; 23 | _fileCopier = fileCopier; 24 | 25 | _watcher = new FileSystemWatcher(source, pattern) 26 | { 27 | NotifyFilter = NotifyFilters.FileName 28 | | NotifyFilters.LastWrite, 29 | 30 | IncludeSubdirectories = recursive, 31 | InternalBufferSize = 64 * 1024, // Max is 64 KB 32 | }; 33 | 34 | _watcher.Changed += OnChanged; 35 | _watcher.Created += OnCreated; 36 | _watcher.Renamed += OnRenamed; 37 | _watcher.Error += OnError; 38 | 39 | DestinationPath = destination; 40 | 41 | _watcher.EnableRaisingEvents = true; 42 | } 43 | 44 | private void OnChanged(object sender, FileSystemEventArgs e) 45 | { 46 | if (e.ChangeType != WatcherChangeTypes.Changed) 47 | { 48 | return; 49 | } 50 | CopyFile(e.FullPath); 51 | } 52 | 53 | private void OnCreated(object sender, FileSystemEventArgs e) => CopyFile(e.FullPath); 54 | 55 | private void OnRenamed(object sender, RenamedEventArgs e) => CopyFile(e.FullPath); 56 | 57 | private void OnError(object sender, ErrorEventArgs e) => WatcherHelpers.PrintException(_logger, e.GetException()); 58 | 59 | private void CopyFile(string fileName) 60 | { 61 | _fileCopier.Add(new(this, fileName)); 62 | } 63 | 64 | public void Dispose() 65 | { 66 | _watcher.Dispose(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Skill2Json/Program.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Formats.Skill; 2 | using HalfLife.UnifiedSdk.Utilities.Logging; 3 | using System.CommandLine; 4 | using System.CommandLine.Parsing; 5 | 6 | namespace HalfLife.UnifiedSdk.Skill2Json 7 | { 8 | internal static class Program 9 | { 10 | const string TargetExtension = ".json"; 11 | 12 | public static async Task Main(string[] args) 13 | { 14 | var inputFileName = new Argument("filename", "skill.cfg to convert"); 15 | 16 | var outputFileName = new Option("--output-filename", 17 | getDefaultValue: () => null, 18 | "If provided, the name of the file to write the skill.json contents to.\n" + 19 | "Otherwise the file is saved to the source directory with the same name and 'json' extension."); 20 | 21 | var rootCommand = new RootCommand("Half-Life Unified SDK skill.cfg to skill.json converter") 22 | { 23 | inputFileName, 24 | outputFileName 25 | }; 26 | 27 | rootCommand.SetHandler((fileName, outputFileName, logger) => 28 | { 29 | if (fileName.Extension == TargetExtension) 30 | { 31 | throw new ArgumentException( 32 | $"Input file \"{fileName.FullName}\" has the same extension as the target file type", nameof(fileName)); 33 | } 34 | 35 | //Default to input with different extension. 36 | outputFileName ??= new FileInfo(Path.ChangeExtension(fileName.FullName, TargetExtension)); 37 | 38 | logger.Information("Writing output to \"{FileName}\"", outputFileName.FullName); 39 | 40 | using var inputStream = fileName.OpenRead(); 41 | using var outputStream = outputFileName.Open(FileMode.Create); 42 | 43 | SkillConverter.Convert(inputStream, outputStream, 44 | $"Converted skill.cfg values from {Path.Combine(fileName.Directory?.Name ?? string.Empty, fileName.Name)}"); 45 | }, 46 | inputFileName, outputFileName, LoggerBinder.Instance); 47 | 48 | return await rootCommand.InvokeAsync(args); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.AssetSynchronizer/Config/AssetManifest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Serilog; 3 | 4 | namespace HalfLife.UnifiedSdk.AssetSynchronizer.Config 5 | { 6 | internal sealed class AssetManifest 7 | { 8 | [JsonProperty(Required = Required.Always)] 9 | public IEnumerable PatternGroups { get; set; } = Enumerable.Empty(); 10 | 11 | public static List Load(ILogger logger, string assetManifestFileName, string assetsDirectoryName, string modDirectoryName) 12 | { 13 | string ResolveSymbols(string input) 14 | { 15 | return input.Replace("%ModDirectory%", modDirectoryName); 16 | } 17 | 18 | var manifest = JsonConvert.DeserializeObject(File.ReadAllText(assetManifestFileName)) ?? new(); 19 | 20 | //Convert paths to absolute. 21 | foreach (var group in manifest.PatternGroups) 22 | { 23 | var groupModDirectoryName = ResolveSymbols(group.Path); 24 | 25 | foreach (var filter in group.Filters) 26 | { 27 | filter.Source = Path.Combine(assetsDirectoryName, filter.Source); 28 | filter.Destination = Path.Combine(groupModDirectoryName, filter.Destination); 29 | 30 | //Verify that the paths are valid. 31 | if (File.Exists(filter.Source)) 32 | { 33 | throw new ConfigException($"The source directory \"{filter.Source}\" is a file"); 34 | } 35 | 36 | if (File.Exists(filter.Destination)) 37 | { 38 | throw new ConfigException($"The destination directory \"{filter.Destination}\" is a file"); 39 | } 40 | 41 | if (!Directory.Exists(filter.Source)) 42 | { 43 | logger.Warning("The source directory \"{Source}\" does not exist and will not be monitored", filter.Source); 44 | } 45 | } 46 | } 47 | 48 | return manifest.PatternGroups 49 | .SelectMany(g => g.Filters) 50 | .Where(f => Directory.Exists(f.Source)) 51 | .ToList(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/RenameEntityClassNamesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 6 | { 7 | /// 8 | /// Renames weapon and item classnames to their primary name. 9 | /// 10 | internal sealed class RenameEntityClassNamesUpgrade : MapUpgrade 11 | { 12 | private static readonly ImmutableDictionary ClassNames = new Dictionary 13 | { 14 | // Old weapon classnames. 15 | ["weapon_glock"] = "weapon_9mmhandgun", 16 | ["ammo_glockclip"] = "ammo_9mmclip", 17 | ["weapon_mp5"] = "weapon_9mmar", 18 | ["ammo_mp5clip"] = "ammo_9mmar", 19 | ["ammo_mp5grenades"] = "ammo_argrenades", 20 | ["weapon_python"] = "weapon_357", 21 | ["weapon_shockroach"] = "weapon_shockrifle", 22 | 23 | // Uppercase conversions. 24 | ["weapon_9mmAR"] = "weapon_9mmar", 25 | ["ammo_9mmAR"] = "ammo_9mmar", 26 | ["ammo_ARgrenades"] = "ammo_argrenades", 27 | ["monster_ShockTrooper_dead"] = "monster_shocktrooper_dead" 28 | }.ToImmutableDictionary(); 29 | 30 | protected override void ApplyCore(MapUpgradeContext context) 31 | { 32 | foreach (var entity in context.Map.Entities) 33 | { 34 | { 35 | if (ClassNames.TryGetValue(entity.ClassName, out var className)) 36 | { 37 | entity.ClassName = className; 38 | } 39 | } 40 | 41 | // Update references in this entity. 42 | if (entity.ClassName == "game_player_equip") 43 | { 44 | foreach (var kv in entity.WithoutClassName().ToList()) 45 | { 46 | if (ClassNames.TryGetValue(kv.Key, out var className)) 47 | { 48 | entity.Remove(kv.Key); 49 | entity.SetString(className, kv.Value); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Logging/LoggerBinder.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using Serilog.Events; 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Binding; 6 | using System.Reflection.Metadata.Ecma335; 7 | 8 | namespace HalfLife.UnifiedSdk.Utilities.Logging 9 | { 10 | /// 11 | /// Provides a in a handler. 12 | /// The logger is configured to log to the console and to the Visual Studio debug output window (when running with Visual Studio). 13 | /// 14 | public sealed class LoggerBinder : BinderBase 15 | { 16 | /// Singleton binder instance. 17 | public static LoggerBinder Instance { get; } = new(); 18 | 19 | private readonly Func _getLogEventLevel; 20 | 21 | /// Creates a binder that uses the log level. 22 | public LoggerBinder() 23 | { 24 | _getLogEventLevel = _ => LogEventLevel.Information; 25 | } 26 | 27 | /// Creates a binder that uses the given option to indicate verbose logging. 28 | public LoggerBinder(Option verboseOption) 29 | { 30 | _getLogEventLevel = bindingContext => 31 | bindingContext.ParseResult.GetValueForOption(verboseOption) ? LogEventLevel.Verbose : LogEventLevel.Information; 32 | } 33 | 34 | /// Creates a binder that uses the given delegate to get the log event level. 35 | public LoggerBinder(Func getLogEventLevel) 36 | { 37 | _getLogEventLevel = getLogEventLevel; 38 | } 39 | 40 | /// 41 | protected override ILogger GetBoundValue(BindingContext bindingContext) 42 | { 43 | return CreateLogger(_getLogEventLevel(bindingContext)); 44 | } 45 | 46 | /// Creates a logger with the given minimum level. 47 | public static ILogger CreateLogger(LogEventLevel minimumLevel) 48 | { 49 | return new LoggerConfiguration() 50 | .WriteTo.Console() 51 | .WriteTo.Debug() 52 | .MinimumLevel.Is(minimumLevel) 53 | .CreateLogger(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeBSPFile/BSPMapBase.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Maps; 3 | using HalfLife.UnifiedSdk.Utilities.Tools; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeBSPFile 8 | { 9 | /// Base class for compiled maps. 10 | internal abstract class BSPMapBase : Map 11 | { 12 | /// The entities lump object containing this map's entity data. 13 | protected readonly Sledge.Formats.Bsp.Lumps.Entities _entitiesLump; 14 | 15 | /// Creates a new map with the given file name and entities lump. 16 | /// is . 17 | /// 18 | /// contains no entities. 19 | /// -or- 's first entity is not worldspawn. 20 | /// -or- has more than one worldspawn. 21 | /// 22 | protected BSPMapBase(string fileName, Sledge.Formats.Bsp.Lumps.Entities entitiesLump) 23 | : base(fileName, MapContentType.Compiled) 24 | { 25 | _entitiesLump = entitiesLump; 26 | } 27 | 28 | protected override EntityList CreateEntities() 29 | { 30 | return new BSPEntityList(this); 31 | } 32 | 33 | internal IEnumerable GetEntities(EntityList entityList) 34 | { 35 | return _entitiesLump 36 | .Select(e => new BSPEntity(entityList, e, e.ClassName == KeyValueUtilities.WorldspawnClassName)) 37 | .ToList(); 38 | } 39 | 40 | internal Entity CreateNewEntity(string className) 41 | { 42 | var entity = new BSPEntity(Entities, new Sledge.Formats.Bsp.Objects.Entity 43 | { 44 | ClassName = className 45 | }, 46 | false); 47 | 48 | _entitiesLump.Add(entity.Entity); 49 | 50 | return entity; 51 | } 52 | 53 | internal void Remove(Entity entity) 54 | { 55 | var bspEntity = (BSPEntity)entity; 56 | _entitiesLump.Remove(bspEntity.Entity); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Logging/MapDiagnostics/MapDiagnosticsBuilder.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.Utilities.Logging.MapDiagnostics 6 | { 7 | /// Builds map diagnostics engines. 8 | public sealed class MapDiagnosticsBuilder 9 | { 10 | private DiagnosticsEventTypes _eventTypes = DiagnosticsEventTypes.None; 11 | 12 | private readonly ImmutableHashSet.Builder _keysToIgnore = 13 | ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); 14 | 15 | internal MapDiagnosticsBuilder() 16 | { 17 | } 18 | 19 | internal MapDiagnosticsEngine Build(ILogger logger, Action? configurator) 20 | { 21 | ArgumentNullException.ThrowIfNull(logger); 22 | 23 | configurator?.Invoke(this); 24 | 25 | return new MapDiagnosticsEngine(logger, _eventTypes, _keysToIgnore.ToImmutable()); 26 | } 27 | 28 | /// Sets the enabled event types to the given types. 29 | public MapDiagnosticsBuilder WithEventTypes(DiagnosticsEventTypes eventTypes) 30 | { 31 | _eventTypes = eventTypes; 32 | return this; 33 | } 34 | 35 | /// Enables the given event types. 36 | public MapDiagnosticsBuilder EnableEventTypes(DiagnosticsEventTypes eventTypes) 37 | { 38 | _eventTypes |= eventTypes; 39 | return this; 40 | } 41 | 42 | /// Disables the given event types. 43 | public MapDiagnosticsBuilder DisableEventTypes(DiagnosticsEventTypes eventTypes) 44 | { 45 | _eventTypes &= ~eventTypes; 46 | return this; 47 | } 48 | 49 | /// Enables all event types. 50 | public MapDiagnosticsBuilder WithAllEventTypes() => WithEventTypes(DiagnosticsEventTypes.All); 51 | 52 | /// Disables all event types. 53 | public MapDiagnosticsBuilder WithNoEventTypes() => WithEventTypes(DiagnosticsEventTypes.None); 54 | 55 | /// Adds the given keys to the set of keys to ignore. 56 | public MapDiagnosticsBuilder IgnoreKeys(params string[] keys) 57 | { 58 | _keysToIgnore.UnionWith(keys); 59 | return this; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife/C3a2bFixWaterValvesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | 4 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.HalfLife 5 | { 6 | /// 7 | /// Prevents players from soft-locking the game by turning both valves at the same time in 8 | /// c3a2b (Lambda Core reactor water flow). 9 | /// 10 | internal sealed class C3a2bFixWaterValvesUpgrade : MapSpecificUpgrade 11 | { 12 | public C3a2bFixWaterValvesUpgrade() 13 | : base("c3a2b") 14 | { 15 | } 16 | 17 | protected override void ApplyCore(MapUpgradeContext context) 18 | { 19 | var valves = context.Map.Entities.Where(e => e.GetTarget() == "ms1" || e.GetTarget() == "ms2"); 20 | var multiManagers = context.Map.Entities.Where(e => e.GetTargetName() == "mm3" || e.GetTargetName() == "mm4"); 21 | 22 | var firstStop = context.Map.Entities.Find("core1"); 23 | var secondStop = context.Map.Entities.Find("core2"); 24 | 25 | if (valves.Count() != 2 || multiManagers.Count() != 2 || firstStop is null || secondStop is null) 26 | { 27 | return; 28 | } 29 | 30 | // Lock the valves while the water is moving 31 | var multisource = context.Map.Entities.CreateNewEntity("multisource"); 32 | 33 | multisource.SetTargetName("valve_ms"); 34 | 35 | var relay = context.Map.Entities.CreateNewEntity("trigger_relay"); 36 | 37 | relay.SetTargetName("valve_ms_relay"); 38 | relay.SetTarget("valve_ms"); 39 | 40 | // Set initial multisource state to enabled 41 | var triggerAuto = context.Map.Entities.CreateNewEntity("trigger_auto"); 42 | 43 | // Remove on fire 44 | triggerAuto.SetSpawnFlags(1); 45 | triggerAuto.SetTarget("valve_ms_relay"); 46 | 47 | foreach (var valve in valves) 48 | { 49 | valve.SetString("master", "valve_ms"); 50 | } 51 | 52 | // Disable valves on start 53 | foreach (var mm in multiManagers) 54 | { 55 | mm.SetDouble("valve_ms_relay", 0); 56 | } 57 | 58 | // Enable when the water stops moving 59 | firstStop.SetString("message", "valve_ms_relay"); 60 | secondStop.SetString("message", "valve_ms_relay"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/OpposingForce/ConvertScientistItemUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.OpposingForce 6 | { 7 | /// 8 | /// Converts the Opposing Force scientist clipboard and stick heads to use the item body group instead. 9 | /// 10 | internal sealed class ConvertScientistItemUpgrade : GameSpecificMapUpgrade 11 | { 12 | //This hardcoded stuff is pretty ugly, but there is no way around it without loading the model. 13 | private const int StudioCount = 1; 14 | private const int HeadsCount = 4; 15 | private const int NeedleCount = 2; 16 | 17 | enum ScientistItem 18 | { 19 | None = 0, 20 | Clipboard, 21 | Stick 22 | } 23 | 24 | public ConvertScientistItemUpgrade() 25 | : base(ValveGames.OpposingForce) 26 | { 27 | } 28 | 29 | protected override void ApplyCore(MapUpgradeContext context) 30 | { 31 | foreach (var scientist in context.Map.Entities 32 | .Where(e => e.ClassName == "monster_scientist" && e.ContainsKey("body"))) 33 | { 34 | var body = scientist.GetInteger("body"); 35 | 36 | var (newBody, item) = DetermineValues(body); 37 | 38 | scientist.SetInteger("item", (int)item); 39 | 40 | scientist.SetInteger("body", newBody); 41 | } 42 | 43 | //Update any generics that use the model. 44 | foreach (var monster in context.Map.Entities 45 | .OfClass("monster_generic") 46 | .Where(e => e.GetModel() == "models/scientist.mdl")) 47 | { 48 | var body = monster.GetInteger("body"); 49 | 50 | var (newBody, item) = DetermineValues(body); 51 | 52 | newBody += StudioCount * HeadsCount * NeedleCount * (int)item; 53 | 54 | monster.SetInteger("body", newBody); 55 | } 56 | } 57 | 58 | private static (int, ScientistItem) DetermineValues(int body) 59 | { 60 | return body switch 61 | { 62 | 4 => (1, ScientistItem.Clipboard), 63 | 5 => (3, ScientistItem.Stick), 64 | _ => (body, ScientistItem.None) 65 | }; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.KeyValueMatcher/KeyValueMatcher.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace HalfLife.UnifiedSdk.KeyValueMatcher 6 | { 7 | /// 8 | /// Matches classnames, keys and values in an entity. 9 | /// 10 | internal sealed class KeyValueMatcher 11 | { 12 | public Regex? ClassNamePattern { get; set; } 13 | 14 | public Regex? KeyPattern { get; set; } 15 | 16 | public Regex? ValuePattern { get; set; } 17 | 18 | public string? FlagsToMatch { get; set; } 19 | 20 | public FlagsMatchMode FlagsMatchMode { get; set; } = FlagsMatchMode.Inclusive; 21 | 22 | public int Flags { get; set; } 23 | 24 | public KeyValuePair? Match(Entity entity) 25 | { 26 | if (ClassNamePattern?.IsMatch(entity.ClassName) == false) 27 | { 28 | return null; 29 | } 30 | 31 | if (KeyPattern is null && ValuePattern is null && FlagsToMatch is null) 32 | { 33 | return new(KeyValueUtilities.ClassName, entity.ClassName); 34 | } 35 | 36 | KeyValuePair result = new(); 37 | 38 | if (KeyPattern is not null || ValuePattern is not null) 39 | { 40 | result = entity.WithoutClassName() 41 | .FirstOrDefault(kv => KeyPattern?.IsMatch(kv.Key) != false && ValuePattern?.IsMatch(kv.Value) != false); 42 | 43 | if (result.Key is null) 44 | { 45 | return null; 46 | } 47 | } 48 | 49 | // Additionally filter by flags if provided. 50 | if (FlagsToMatch is not null) 51 | { 52 | var flags = entity.GetInteger(FlagsToMatch); 53 | 54 | bool containsFlag = (flags & Flags) != 0; 55 | 56 | if (FlagsMatchMode == FlagsMatchMode.Inclusive) 57 | { 58 | if (!containsFlag) 59 | { 60 | return null; 61 | } 62 | } 63 | else 64 | { 65 | if (containsFlag) 66 | { 67 | return null; 68 | } 69 | } 70 | 71 | // No key to match against, so use flags instead. 72 | if (result.Key is null) 73 | { 74 | result = new(FlagsToMatch, flags.ToString()); 75 | } 76 | } 77 | 78 | return result; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/BlueShift/ChangeBlueShiftSentencesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Games; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 4 | using System.Collections.Immutable; 5 | 6 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.BlueShift 7 | { 8 | /// 9 | /// Updates references to specific sentences to use the correct vanilla Half-Life sentence. 10 | /// 11 | internal sealed class ChangeBlueShiftSentencesUpgrade : MapUpgrade 12 | { 13 | private static readonly ImmutableDictionary SentenceMap = new Dictionary 14 | { 15 | // The NA group is for No Access, EA is for Enable Access. 16 | // BS incorrectly adds an access granted sentence to the NA group. 17 | { "!NA1", "!EA0" }, 18 | // ba_tram2 uses these two; they don't exist in Blue Shift, 19 | // but do in Half-Life and refer to Freeman and the HEV suit. 20 | { "!SC_HELLO6", "NULLSENT" }, 21 | { "!SC_HELLO8", "NULLSENT" }, 22 | } 23 | // Renames HOLO_* sentences to BSHOLO_*. 24 | .Concat(new[] 25 | { 26 | "HOLO_4JUMPS", 27 | "HOLO_ARMOR", 28 | "HOLO_BREAKBOX", 29 | "HOLO_BREATH", 30 | "HOLO_BUTTON", 31 | "HOLO_CHARGER", 32 | "HOLO_COMMENCING", 33 | "HOLO_CONGRATS", 34 | "HOLO_DONE", 35 | "HOLO_DROWN", 36 | "HOLO_DUCK", 37 | "HOLO_FALLSHORT", 38 | "HOLO_FANTASTIC", 39 | "HOLO_FLASHLIGHT", 40 | "HOLO_GREATWORK", 41 | "HOLO_GRENADE", 42 | "HOLO_HITALL", 43 | "HOLO_INJURY", 44 | "HOLO_INTRO", 45 | "HOLO_JDUCK", 46 | "HOLO_JUMP", 47 | "HOLO_JUMPDOWN", 48 | "HOLO_JUMPGAP", 49 | "HOLO_KEEPTRYING", 50 | "HOLO_LADDER", 51 | "HOLO_LEADGUARD", 52 | "HOLO_LIGHTOFF", 53 | "HOLO_LONGJUMP", 54 | "HOLO_MEDKIT", 55 | "HOLO_MOVE", 56 | "HOLO_NICEJOB", 57 | "HOLO_PIPEDUCK", 58 | "HOLO_PULLBOX", 59 | "HOLO_PUSHBOX", 60 | "HOLO_RADIATION", 61 | "HOLO_RETRY", 62 | "HOLO_RUNSTART", 63 | "HOLO_SPINBRIDGE", 64 | "HOLO_STARTLIFT", 65 | "HOLO_STEAM", 66 | "HOLO_TARGET", 67 | "HOLO_TRYAGAIN", 68 | "HOLO_USETRAIN" 69 | }.Select(s => new KeyValuePair("!" + s, "!BS" + s))) 70 | .ToImmutableDictionary(StringComparer.OrdinalIgnoreCase); 71 | 72 | protected override void ApplyCore(MapUpgradeContext context) 73 | { 74 | if (context.GameInfo != ValveGames.BlueShift) 75 | { 76 | return; 77 | } 78 | 79 | SentenceUtilities.ReplaceSentences(context.Map.Entities, SentenceMap); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Bsp2Obj/Program.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Logging; 2 | using Sledge.Formats.Bsp; 3 | using System.CommandLine; 4 | 5 | namespace HalfLife.UnifiedSdk.Bsp2Obj 6 | { 7 | internal static class Program 8 | { 9 | public static async Task Main(string[] args) 10 | { 11 | var bspFileNameArgument = new Argument("filename", description: "Path to the BSP file"); 12 | 13 | var destinationDirectoryOption = new Option("--destination", 14 | getDefaultValue: () => null, 15 | description: "Directory to save the OBJ, material and texture files to." 16 | + " If not provided the files will be saved to the directory that the source file is located in"); 17 | 18 | var rootCommand = new RootCommand("Half-Life Unified SDK BSP to OBJ converter") 19 | { 20 | bspFileNameArgument, 21 | destinationDirectoryOption 22 | }; 23 | 24 | rootCommand.SetHandler((bspFileName, destinationDirectory, logger) => 25 | { 26 | if (!bspFileName.Exists) 27 | { 28 | logger.Error("The given bsp file \"{BspFileName}\" does not exist", bspFileName); 29 | return; 30 | } 31 | 32 | destinationDirectory ??= bspFileName.Directory; 33 | 34 | if (destinationDirectory is null) 35 | { 36 | logger.Error("Destination directory is invalid"); 37 | return; 38 | } 39 | 40 | BspFile bspFile; 41 | 42 | try 43 | { 44 | logger.Information("Reading BSP file {FileName}", bspFileName.FullName); 45 | using var stream = bspFileName.OpenRead(); 46 | bspFile = new(stream); 47 | } 48 | catch (Exception e) 49 | { 50 | logger.Error(e, "An error occurred while reading the BSP file"); 51 | return; 52 | } 53 | 54 | try 55 | { 56 | destinationDirectory.Create(); 57 | } 58 | catch (Exception e) 59 | { 60 | logger.Error(e, "An error occurred while creating the destination directory"); 61 | return; 62 | } 63 | 64 | logger.Information("Converting BSP data to OBJ"); 65 | 66 | try 67 | { 68 | BspToObjConverter.Convert(logger, destinationDirectory.FullName, bspFileName.FullName, bspFile); 69 | } 70 | catch (Exception e) 71 | { 72 | logger.Error(e, "An error occurred while writing the obj file"); 73 | return; 74 | } 75 | }, bspFileNameArgument, destinationDirectoryOption, LoggerBinder.Instance); 76 | 77 | return await rootCommand.InvokeAsync(args); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Packager/Config/PackageManifest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Serilog; 3 | 4 | namespace HalfLife.UnifiedSdk.Packager.Config 5 | { 6 | internal sealed class PackageManifest 7 | { 8 | [JsonProperty(Required = Required.Always)] 9 | public IEnumerable PatternGroups { get; set; } = Enumerable.Empty(); 10 | 11 | public static IEnumerable Load(ILogger logger, string packageManifestFileName, string rootDirectory, string modDirectoryName) 12 | { 13 | string ResolveSymbols(string input) 14 | { 15 | return input.Replace("%ModDirectory%", modDirectoryName); 16 | } 17 | 18 | var manifest = JsonConvert.DeserializeObject(File.ReadAllText(packageManifestFileName)); 19 | 20 | if (manifest is null || !manifest.PatternGroups.Any()) 21 | { 22 | throw new ConfigException("Manifest file is empty"); 23 | } 24 | 25 | //Resolve paths and make them absolute. 26 | foreach (var directory in manifest.PatternGroups) 27 | { 28 | directory.Paths = directory.Paths.ConvertAll(p => 29 | { 30 | var path = p.Path; 31 | 32 | path = ResolveSymbols(path); 33 | path = Path.Combine(rootDirectory, path); 34 | 35 | p.Path = path; 36 | 37 | return p; 38 | }); 39 | 40 | directory.IncludePatterns = directory.IncludePatterns.ConvertAll(ResolveSymbols); 41 | directory.ExcludePatterns = directory.ExcludePatterns.ConvertAll(ResolveSymbols); 42 | } 43 | 44 | //Check if any required paths don't exist. 45 | var flattened = manifest.PatternGroups.SelectMany(d => d.Paths.Select(p => new 46 | { 47 | p.Path, 48 | p.Required, 49 | Exists = Directory.Exists(p.Path), 50 | d.IncludePatterns, 51 | d.ExcludePatterns 52 | })); 53 | 54 | foreach (var directory in flattened) 55 | { 56 | if (directory.Exists) 57 | { 58 | logger.Information("Including directory \"{Path}\"", directory.Path); 59 | } 60 | else 61 | { 62 | if (!directory.Required) 63 | { 64 | logger.Information("Directory \"{Path}\" is optional and does not exist, skipping", directory.Path); 65 | } 66 | else 67 | { 68 | throw new ConfigException($"Directory \"{directory.Path}\" is required and does not exist, aborting"); 69 | } 70 | } 71 | } 72 | 73 | return flattened 74 | .Where(d => d.Exists) 75 | .Select(d => new PackageDirectory(d.Path, d.IncludePatterns, d.ExcludePatterns)) 76 | .ToList(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Tools/ScriptedSequenceUtilities.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System; 4 | using System.Collections.Immutable; 5 | using System.Linq; 6 | 7 | namespace HalfLife.UnifiedSdk.Utilities.Tools 8 | { 9 | /// Utility functionality for scripted_sequence. 10 | public static class ScriptedSequenceUtilities 11 | { 12 | /// The scripted_sequence classname. 13 | public const string ClassName = "scripted_sequence"; 14 | 15 | /// The m_iszEntity key name. 16 | public const string TargetKey = "m_iszEntity"; 17 | 18 | /// The m_iszIdle key name. 19 | public const string IdleKey = "m_iszIdle"; 20 | 21 | /// The m_iszPlay key name. 22 | public const string PlayKey = "m_iszPlay"; 23 | 24 | /// List of keys to check for animation names. 25 | public static readonly ImmutableList KeysToCheck = ImmutableList.Create(IdleKey, PlayKey); 26 | 27 | /// 28 | /// Given an NPC class name, model name and a dictionary of animation names, renames all animations used in scripted sequences. 29 | /// 30 | /// Context to operate on. 31 | /// Class name of the NPC to check. Can be null if there is no class. 32 | /// Name of the model to check. 33 | /// Dictionary of animation names to rename. 34 | /// 35 | /// If , or are null. 36 | /// 37 | public static void RenameAnimations( 38 | MapUpgradeContext context, string? npcName, string modelName, ImmutableDictionary animationRemap) 39 | { 40 | ArgumentNullException.ThrowIfNull(context); 41 | ArgumentNullException.ThrowIfNull(modelName); 42 | ArgumentNullException.ThrowIfNull(animationRemap); 43 | 44 | foreach (var script in context.Map.Entities 45 | .OfClass(ClassName) 46 | .Where(e => e.ContainsKey(TargetKey))) 47 | { 48 | var target = context.Map.Entities 49 | .WhereTargetName(script.GetString(TargetKey)) 50 | .FirstOrDefault(); 51 | 52 | if (target is null) 53 | { 54 | continue; 55 | } 56 | 57 | if (target.ClassName != npcName && target.GetModel() != modelName) 58 | { 59 | continue; 60 | } 61 | 62 | foreach (var key in KeysToCheck) 63 | { 64 | if (script.GetStringOrNull(key) is { } animation 65 | && animationRemap.TryGetValue(animation, out var replacement)) 66 | { 67 | script.SetString(key, replacement); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/HalfLife/C2a5FixBarrelPushTriggersUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 3 | using System.Collections.Immutable; 4 | 5 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.HalfLife 6 | { 7 | /// 8 | /// Fixes the barrels in c2a5 not flying as high as they're supposed to on modern systems due to high framerates. 9 | /// 10 | internal sealed class C2a5FixBarrelPushTriggersUpgrade : MapSpecificUpgrade 11 | { 12 | [Flags] 13 | private enum TriggerPushSpawnFlag 14 | { 15 | None = 0, 16 | OnceOnly = 1 << 0, 17 | } 18 | 19 | private record BarrelSetup(string MultiManagerName, string PushName, string BarrelName); 20 | 21 | private static readonly ImmutableArray BarrelSetups = ImmutableArray.Create( 22 | new BarrelSetup("can_expl2_mm", "can_expl2_push", "can_expl2_shoot"), 23 | new BarrelSetup("can_expl4_mm", "can_expl4_push", "can_expl4_shoot"), 24 | new BarrelSetup("can_expl5_mm", "can_expl5_push", "can_expl5_shoot")); 25 | 26 | public C2a5FixBarrelPushTriggersUpgrade() 27 | : base("c2a5") 28 | { 29 | } 30 | 31 | protected override void ApplyCore(MapUpgradeContext context) 32 | { 33 | foreach (var setup in BarrelSetups) 34 | { 35 | var multiManager = context.Map.Entities.Find(setup.MultiManagerName); 36 | var push = context.Map.Entities.Find(setup.PushName); 37 | var pushable = context.Map.Entities.Find(setup.BarrelName); 38 | 39 | if (multiManager is null || push is null || pushable is null) 40 | { 41 | continue; 42 | } 43 | 44 | // Remove the second push trigger to avoid re-enabling it. 45 | multiManager.Remove($"{setup.PushName}#1"); 46 | 47 | var spawnFlags = push.GetSpawnFlags(); 48 | spawnFlags |= (int)TriggerPushSpawnFlag.OnceOnly; 49 | push.SetSpawnFlags(spawnFlags); 50 | 51 | // Nudge this pushable up by a unit. 52 | // This prevents the pushable getting stuck in the ground. 53 | // If the player uses a radius damage attack directed at the middle of the road 54 | // (dead ahead exiting the tunnel) 55 | // the specific entity setup used here won't allow the pushable to fly up otherwise 56 | // because the engine thinks the pushable is somewhere else/outside the world. 57 | // Specifically, when breakables are destroyed they clear the groundentity variable for entities 58 | // (see CBreakable::Die) 59 | // This particular entity setup happens to cause the variable to be cleared at just the right time 60 | // for the pushable to touch the push trigger. 61 | // This doesn't happen if a RadiusDamage attack is used due to slight timing changes. 62 | // Only can_expl2_shoot has shown this behavior but just to be safe all of them are modified. 63 | var origin = pushable.GetOrigin(); 64 | ++origin.Z; 65 | pushable.SetOrigin(origin); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Entities/EnumerableEntityExtensions.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Tools; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace HalfLife.UnifiedSdk.Utilities.Entities 7 | { 8 | /// Extensions for of . 9 | public static class EnumerableEntityExtensions 10 | { 11 | /// Returns an enumerable collection of entities whose classname matches the given name. 12 | /// 13 | /// or are . 14 | /// 15 | public static IEnumerable OfClass(this IEnumerable entityList, string className) 16 | { 17 | ArgumentNullException.ThrowIfNull(entityList); 18 | ArgumentNullException.ThrowIfNull(className); 19 | 20 | return entityList.Where(e => e.ClassName == className); 21 | } 22 | 23 | /// Finds all entities that have the given key and value 24 | /// 25 | /// , or are . 26 | /// 27 | public static IEnumerable WhereString(this IEnumerable entityList, string key, string value) 28 | { 29 | ArgumentNullException.ThrowIfNull(entityList); 30 | ArgumentNullException.ThrowIfNull(key); 31 | ArgumentNullException.ThrowIfNull(value); 32 | 33 | return entityList.Where(e => e.HasKeyValueCore(key, value)); 34 | } 35 | 36 | /// Finds all entities that have the given targetname. 37 | /// 38 | /// or are . 39 | /// 40 | public static IEnumerable WhereTargetName(this IEnumerable entityList, string targetName) 41 | { 42 | return entityList.WhereString(KeyValueUtilities.TargetName, targetName); 43 | } 44 | 45 | /// Finds all entities that have the given target. 46 | /// 47 | /// or are . 48 | /// 49 | public static IEnumerable WhereTarget(this IEnumerable entityList, string target) 50 | { 51 | return entityList.WhereString(KeyValueUtilities.Target, target); 52 | } 53 | 54 | /// Gets an enumerable collection of all entities except worldspawn. 55 | /// is . 56 | public static IEnumerable WithoutWorldspawn(this IEnumerable entityList) 57 | { 58 | ArgumentNullException.ThrowIfNull(entityList); 59 | 60 | //Don't assume worldspawn is the first entity (in case of lists that have been reorganized). 61 | //There could be multiple instances of it in the list. 62 | return entityList.Where(e => !e.IsWorldspawn); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader.Upgrades/Common/AdjustShotgunAnglesUpgrade.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Tools; 4 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 5 | using System.Numerics; 6 | 7 | namespace HalfLife.UnifiedSdk.MapUpgrader.Upgrades.Common 8 | { 9 | /// 10 | /// Opposing Force and Blue Shift's shotgun world model has a different alignment. 11 | /// Since we're using the vanilla Half-Life model the angles need adjusting. 12 | /// This adjusts the angles to match the original model. 13 | /// 14 | internal sealed class AdjustShotgunAnglesUpgrade : MapUpgrade 15 | { 16 | private const string ShotgunModelName = "models/w_shotgun.mdl"; 17 | private static readonly Vector3 AngleAdjust = new(0, 180, 90); 18 | private static readonly Vector3 Security2AngleAdjust = new(270, 180, 0); 19 | 20 | protected override void ApplyCore(MapUpgradeContext context) 21 | { 22 | if (context.GameInfo != ValveGames.OpposingForce && context.GameInfo != ValveGames.BlueShift) 23 | { 24 | return; 25 | } 26 | 27 | foreach (var entity in context.Map.Entities.OfClass("weapon_shotgun")) 28 | { 29 | UpdateAngles(entity, AngleAdjust); 30 | UpdateOrigin(entity, entity.GetOrigin()); 31 | } 32 | 33 | if (context.Map.BaseName == "ba_security2") 34 | { 35 | foreach (var script in context.Map.Entities 36 | .OfClass(ScriptedSequenceUtilities.ClassName) 37 | .Where(e => e.ContainsKey(ScriptedSequenceUtilities.TargetKey)) 38 | .ToList()) 39 | { 40 | var shotgun = context.Map.Entities 41 | .WhereTargetName(script.GetString(ScriptedSequenceUtilities.TargetKey)) 42 | .FirstOrDefault(); 43 | 44 | if (shotgun is null 45 | || shotgun.ClassName != "monster_generic" 46 | || shotgun.GetModel() != ShotgunModelName) 47 | { 48 | continue; 49 | } 50 | 51 | shotgun.ClassName = "item_generic"; 52 | //Wipe all spawnflags since they don't match between the two. 53 | shotgun.SetSpawnFlags(0); 54 | 55 | UpdateAngles(shotgun, Security2AngleAdjust); 56 | UpdateOrigin(shotgun, script.GetOrigin()); 57 | 58 | //No longer needed. 59 | context.Map.Entities.Remove(script); 60 | } 61 | } 62 | } 63 | 64 | private static void UpdateAngles(Entity entity, Vector3 adjust) 65 | { 66 | var angles = entity.GetAngles(); 67 | 68 | angles += adjust; 69 | 70 | entity.SetAngles(angles); 71 | } 72 | 73 | private static void UpdateOrigin(Entity entity, Vector3 newOrigin) 74 | { 75 | //Adjust the vertical position by 2 units to match the original model. 76 | var angles = entity.GetAngles(); 77 | 78 | MathUtilities.AngleVectors(angles, out _, out _, out var up); 79 | 80 | newOrigin += up * -2; 81 | 82 | entity.SetOrigin(newOrigin); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.MapUpgrader/Program.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.MapUpgrader.Upgrades; 2 | using HalfLife.UnifiedSdk.Utilities.Games; 3 | using HalfLife.UnifiedSdk.Utilities.Logging; 4 | using HalfLife.UnifiedSdk.Utilities.Tools; 5 | using HalfLife.UnifiedSdk.Utilities.Tools.UpgradeTool; 6 | using System.CommandLine; 7 | 8 | namespace HalfLife.UnifiedSdk.MapUpgrader 9 | { 10 | internal static class Program 11 | { 12 | public static int Main(string[] args) 13 | { 14 | var games = ValveGames.GoldSourceGames; 15 | 16 | var defaultGame = ValveGames.HalfLife1; 17 | 18 | var gameOption = new Option("--game", 19 | getDefaultValue: () => defaultGame.ModDirectory, 20 | description: "The name of a game's mod directory to apply upgrades for that game"); 21 | 22 | gameOption.AddCompletions(games.Select(g => g.ModDirectory).ToArray()); 23 | 24 | gameOption.AddValidator((result) => 25 | { 26 | var game = result.GetValueForOption(gameOption)!; 27 | 28 | if (!games.Any(g => g.ModDirectory == game)) 29 | { 30 | result.ErrorMessage = $"Invalid game \"{game}\" specified."; 31 | } 32 | }); 33 | 34 | var diagnosticsLevelOption = new Option("--diagnostics-level", 35 | getDefaultValue: () => DiagnosticsLevel.Disabled, 36 | description: "The diagnostics level to set"); 37 | 38 | var mapsArgument = new Argument>("maps", description: "List of maps to upgrade"); 39 | 40 | var rootCommand = new RootCommand("Half-Life Unified SDK map upgrader") 41 | { 42 | gameOption, 43 | diagnosticsLevelOption, 44 | mapsArgument 45 | }; 46 | 47 | rootCommand.SetHandler((game, diagnosticsLevel, maps, logger) => 48 | { 49 | var gameInfo = games.SingleOrDefault(g => g.ModDirectory == game); 50 | 51 | if (gameInfo is null) 52 | { 53 | //Should never get here. 54 | return; 55 | } 56 | 57 | var upgradeTool = MapUpgradeToolFactory.Create(logger, diagnosticsLevel); 58 | 59 | logger.Information("Upgrading maps for game {GameName} ({ModDirectory}) to version {LatestVersion}", 60 | gameInfo.Name, gameInfo.ModDirectory, upgradeTool.LatestVersion); 61 | 62 | if (!maps.Any()) 63 | { 64 | logger.Information("No maps to upgrade"); 65 | return; 66 | } 67 | 68 | foreach (var map in maps) 69 | { 70 | var mapData = MapFormats.Deserialize(map.FullName); 71 | 72 | var currentVersion = upgradeTool.GetVersion(mapData); 73 | 74 | logger.Information("Upgrading \"{FullName}\" from version {CurrentVersion}", map.FullName, currentVersion); 75 | 76 | upgradeTool.Upgrade(new MapUpgradeCommand(mapData, gameInfo)); 77 | 78 | using var stream = File.Open(map.FullName, FileMode.Create, FileAccess.Write); 79 | 80 | mapData.Serialize(stream); 81 | } 82 | }, gameOption, diagnosticsLevelOption, mapsArgument, LoggerBinder.Instance); 83 | 84 | return rootCommand.Invoke(args); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/HalfLife.UnifiedSdk.Utilities/Serialization/SledgeMapFile/MapFileEntity.cs: -------------------------------------------------------------------------------- 1 | using HalfLife.UnifiedSdk.Utilities.Entities; 2 | using HalfLife.UnifiedSdk.Utilities.Tools; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | 9 | namespace HalfLife.UnifiedSdk.Utilities.Serialization.SledgeMapFile 10 | { 11 | internal sealed class MapFileEntity : Entity 12 | { 13 | public Sledge.Formats.Map.Objects.Entity Entity { get; } 14 | 15 | public override bool IsWorldspawn { get; } 16 | 17 | public MapFileEntity(EntityList entityList, Sledge.Formats.Map.Objects.Entity entity, bool isWorldspawn) 18 | : base(entityList, entity.SortedProperties 19 | .Append(new KeyValuePair(KeyValueUtilities.ClassName, entity.ClassName)) 20 | .Append(new KeyValuePair(KeyValueUtilities.SpawnFlags, entity.SpawnFlags.ToString())) 21 | .ToImmutableList()) 22 | { 23 | Entity = entity; 24 | IsWorldspawn = isWorldspawn; 25 | } 26 | 27 | protected override void SetKeyValue(int index, string key, string value, bool overwrite) 28 | { 29 | switch (key) 30 | { 31 | case KeyValueUtilities.ClassName: 32 | Entity.ClassName = value; 33 | break; 34 | 35 | case KeyValueUtilities.SpawnFlags: 36 | Entity.SpawnFlags = int.Parse(value); 37 | break; 38 | 39 | default: 40 | { 41 | index = GetAdjustedIndex(index); 42 | 43 | if (overwrite) 44 | { 45 | Entity.SortedProperties[index] = new(key, value); 46 | } 47 | else 48 | { 49 | Entity.SortedProperties.Insert(index, new(key, value)); 50 | } 51 | break; 52 | } 53 | } 54 | } 55 | 56 | protected override void RemoveKeyValue(int index, string key) 57 | { 58 | switch (key) 59 | { 60 | case KeyValueUtilities.ClassName: 61 | throw new ArgumentException("Cannot remove the classname keyvalue"); 62 | 63 | case KeyValueUtilities.SpawnFlags: 64 | Entity.SpawnFlags = 0; 65 | break; 66 | 67 | default: 68 | index = GetAdjustedIndex(index); 69 | Entity.SortedProperties.RemoveAt(index); 70 | break; 71 | } 72 | } 73 | 74 | protected override void RemoveAllKeyValues() 75 | { 76 | Entity.SortedProperties.Clear(); 77 | } 78 | 79 | private int GetAdjustedIndex(int index) 80 | { 81 | var classnameIndex = IndexOf(KeyValueUtilities.ClassName); 82 | var spawnflagsIndex = IndexOf(KeyValueUtilities.SpawnFlags); 83 | 84 | Debug.Assert(index != classnameIndex); 85 | Debug.Assert(index != spawnflagsIndex); 86 | 87 | if (classnameIndex < index) 88 | { 89 | --index; 90 | } 91 | 92 | if (spawnflagsIndex != -1 && spawnflagsIndex <= index) 93 | { 94 | --index; 95 | } 96 | 97 | return index; 98 | } 99 | } 100 | } 101 | --------------------------------------------------------------------------------