diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67824f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,407 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/visualstudio + +*.targets diff --git a/SMShared/Json/ModInfoJson.cs b/SMShared/Json/ModInfoJson.cs index be769f4..6d7937e 100644 --- a/SMShared/Json/ModInfoJson.cs +++ b/SMShared/Json/ModInfoJson.cs @@ -1,18 +1,14 @@ using System; -#if PACKAGER -#nullable disable -#endif - namespace SMShared.Json { [Serializable] public class ModInfoJson { - public string Id; - public string DisplayName; + public string? Id; + public string? DisplayName; public string Version = "1.0.0"; - public string Author; + public string? Author; public readonly string ManagerVersion = "0.27.3"; public readonly string[] Requirements = { "SkinManagerMod" }; } diff --git a/SMShared/Json/ResourceConfigJson.cs b/SMShared/Json/ResourceConfigJson.cs index 610f0ba..0a40c56 100644 --- a/SMShared/Json/ResourceConfigJson.cs +++ b/SMShared/Json/ResourceConfigJson.cs @@ -1,8 +1,6 @@ using System; -#if PACKAGER #nullable disable -#endif namespace SMShared.Json { diff --git a/SMShared/Json/SkinConfigJson.cs b/SMShared/Json/SkinConfigJson.cs index e1223eb..73d549c 100644 --- a/SMShared/Json/SkinConfigJson.cs +++ b/SMShared/Json/SkinConfigJson.cs @@ -1,15 +1,11 @@ using System; -#if PACKAGER -#nullable disable -#endif - namespace SMShared.Json { [Serializable] public class SkinConfigJson : ResourceConfigJson { - public string[] ResourceNames; + public string[]? ResourceNames; public BaseTheme BaseTheme = BaseTheme.DVRT; } diff --git a/SMShared/Json/ThemeConfigJson.cs b/SMShared/Json/ThemeConfigJson.cs index 1ab647d..a7cfe38 100644 --- a/SMShared/Json/ThemeConfigJson.cs +++ b/SMShared/Json/ThemeConfigJson.cs @@ -2,30 +2,26 @@ using System.Collections.Generic; using System.Text; -#if PACKAGER -#nullable disable -#endif - namespace SMShared.Json { [Serializable] public class ThemeConfigJson { - public string Version = "1.0.0"; - public ThemeConfigItem[] Themes; + public string? Version = "1.0.0"; + public ThemeConfigItem[]? Themes; } [Serializable] public class ThemeConfigItem { - public string Name; + public string? Name; public bool HideFromStores; public bool PreventRandomSpawning; public float? CanPrice; - public string LabelTextureFile; - public string LabelBaseColor; - public string LabelAccentColorA; - public string LabelAccentColorB; + public string? LabelTextureFile; + public string? LabelBaseColor; + public string? LabelAccentColorA; + public string? LabelAccentColorB; } } diff --git a/SMShared/Remaps.cs b/SMShared/Remaps.cs index 9f83fe2..e4e685b 100644 --- a/SMShared/Remaps.cs +++ b/SMShared/Remaps.cs @@ -1,10 +1,4 @@ -#if PACKAGER - #nullable disable -#else - using DV.ThingTypes; -#endif - -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -15,7 +9,7 @@ public static class Remaps { private static readonly Dictionary _newToOldCarIdMap; - public static bool TryGetOldTrainCarId(string newId, out string oldId) + public static bool TryGetOldTrainCarId(string newId, out string? oldId) { return _newToOldCarIdMap.TryGetValue(newId, out oldId); } @@ -67,7 +61,7 @@ public static bool TryGetOldTrainCarId(string newId, out string oldId) { "CarNuclearFlask", "NuclearFlask" }, }; - public static bool TryGetUpdatedCarId(string oldId, out string newId) + public static bool TryGetUpdatedCarId(string oldId, out string? newId) { return _oldToNewCarIdMap.TryGetValue(oldId, out newId); } @@ -100,7 +94,7 @@ public IEnumerator> GetEnumerator() return ((IEnumerable>)_map).GetEnumerator(); } - public bool TryGetUpdatedName(string oldName, out string newName) + public bool TryGetUpdatedName(string oldName, out string? newName) { return _map.TryGetValue(oldName, out newName); } @@ -255,9 +249,9 @@ static Remaps() }); } - public static bool TryGetUpdatedTextureName(string liveryId, string oldName, out string newName) + public static bool TryGetUpdatedTextureName(string liveryId, string oldName, out string? newName) { - if (_legacyTextureNameMap.TryGetValue(liveryId, out TextureMapping textureMapping)) + if (_legacyTextureNameMap.TryGetValue(liveryId, out TextureMapping? textureMapping)) { return textureMapping.TryGetUpdatedName(oldName, out newName); } @@ -266,8 +260,4 @@ public static bool TryGetUpdatedTextureName(string liveryId, string oldName, out return false; } } -} - -#if PACKAGER - #nullable restore -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/SkinConfigurator/FolderPackager.cs b/SkinConfigurator/FolderPackager.cs index 8fa6e58..e0c57e2 100644 --- a/SkinConfigurator/FolderPackager.cs +++ b/SkinConfigurator/FolderPackager.cs @@ -1,13 +1,8 @@ using SkinConfigurator.ViewModels; using SMShared; using SMShared.Json; -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace SkinConfigurator { @@ -75,7 +70,7 @@ protected override void WriteThemeConfig() for (int i = 0; i < _model.ThemeConfigs.Count; i++) { var config = _model.ThemeConfigs[i]; - if (config.HideFromStores) + if (config.HasValidImage) { string destPath = config.PackagedLabelTexturePath; File.Copy(config.TempPath, destPath, true); diff --git a/SkinConfigurator/ResourceSelector.xaml.cs b/SkinConfigurator/ResourceSelector.xaml.cs index 197052e..de4dec1 100644 --- a/SkinConfigurator/ResourceSelector.xaml.cs +++ b/SkinConfigurator/ResourceSelector.xaml.cs @@ -91,7 +91,7 @@ public void RefreshAvailableItems() if ((SkinPack is not null) && (Skin is not null)) { available = SkinPack.PackComponents - .Where(c => (c.Type == PackComponentType.Resource) && !string.IsNullOrWhiteSpace(c.Name) && (c.CarId == Skin.CarId)) + .Where(c => (c.Type == PackComponentType.Resource) && !string.IsNullOrWhiteSpace(c.Name)) .OrderBy(c => c.Name) .ToList(); diff --git a/SkinConfigurator/ViewModels/PackComponentModel.cs b/SkinConfigurator/ViewModels/PackComponentModel.cs index 0f46cfc..fcccaa9 100644 --- a/SkinConfigurator/ViewModels/PackComponentModel.cs +++ b/SkinConfigurator/ViewModels/PackComponentModel.cs @@ -33,7 +33,7 @@ public ResourceConfigJson JsonModel() { Name = Name, CarId = CarId, - ResourceNames = Resources?.Select(r => r.Name).ToArray(), + ResourceNames = Resources?.Select(r => r.Name!).ToArray(), BaseTheme = BaseTheme, }; } diff --git a/SkinConfigurator/ViewModels/SkinFileModel.cs b/SkinConfigurator/ViewModels/SkinFileModel.cs index 67a4642..2c77883 100644 --- a/SkinConfigurator/ViewModels/SkinFileModel.cs +++ b/SkinConfigurator/ViewModels/SkinFileModel.cs @@ -26,7 +26,7 @@ public string FileName set { SetValue(FileNameProperty, value); - CanUpgradeFileName = Remaps.TryGetUpdatedTextureName(Parent.CarId, Path.GetFileNameWithoutExtension(value), out _); + CanUpgradeFileName = Remaps.TryGetUpdatedTextureName(Parent.CarId!, Path.GetFileNameWithoutExtension(value), out _); FileNameChanged?.Invoke(); } } @@ -61,7 +61,7 @@ public void UpgradeFileName() { string name = Path.GetFileNameWithoutExtension(FileName); - if (Remaps.TryGetUpdatedTextureName(Parent.CarId, name, out string newName)) + if (Remaps.TryGetUpdatedTextureName(Parent.CarId!, name, out string? newName)) { FileName = $"{newName}{Extension}"; } diff --git a/SkinConfigurator/ViewModels/SkinModInfoModel.cs b/SkinConfigurator/ViewModels/SkinModInfoModel.cs index 6071710..ef0e102 100644 --- a/SkinConfigurator/ViewModels/SkinModInfoModel.cs +++ b/SkinConfigurator/ViewModels/SkinModInfoModel.cs @@ -20,7 +20,7 @@ public ModInfoJson JsonModel() { Id = Id, DisplayName = DisplayName, - Version = Version, + Version = Version!, Author = string.IsNullOrWhiteSpace(Author) ? null : Author, }; } diff --git a/SkinConfigurator/ViewModels/ThemeConfigModel.cs b/SkinConfigurator/ViewModels/ThemeConfigModel.cs index d621102..8e87e56 100644 --- a/SkinConfigurator/ViewModels/ThemeConfigModel.cs +++ b/SkinConfigurator/ViewModels/ThemeConfigModel.cs @@ -158,6 +158,10 @@ public ThemeConfigModel(SkinPackModel parent, ThemeConfigItem json, string dirPa { ParentPack = parent; ThemeName = json.Name; + HideFromStores = json.HideFromStores; + PreventRandomSpawning = json.PreventRandomSpawning; + CanPrice = json.CanPrice; + if (!string.IsNullOrEmpty(json.LabelTextureFile)) { string texturePath = Path.Combine(dirPath, json.LabelTextureFile); @@ -169,7 +173,7 @@ public ThemeConfigModel(SkinPackModel parent, ThemeConfigItem json, string dirPa LabelAccentColorB = TryParseColor(json.LabelAccentColorB); } - private static Color TryParseColor(string value) + private static Color TryParseColor(string? value) { if (!string.IsNullOrEmpty(value)) { diff --git a/SkinManagerMod.sln b/SkinManagerMod.sln index 2202ccd..b049aaf 100644 --- a/SkinManagerMod.sln +++ b/SkinManagerMod.sln @@ -32,7 +32,7 @@ Global EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution SMShared\SMShared.projitems*{3c5dfee2-5da9-4674-96d8-c1503efca3c0}*SharedItemsImports = 5 - SMShared\SMShared.projitems*{bbdceb20-83d5-4fc8-88f9-0e08df1b1dde}*SharedItemsImports = 4 + SMShared\SMShared.projitems*{bbdceb20-83d5-4fc8-88f9-0e08df1b1dde}*SharedItemsImports = 5 SMShared\SMShared.projitems*{f951746d-cb16-45ee-80a7-01e07ac461be}*SharedItemsImports = 13 EndGlobalSection EndGlobal diff --git a/SkinManagerMod/.gitignore b/SkinManagerMod/.gitignore deleted file mode 100644 index cd42ee3..0000000 --- a/SkinManagerMod/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -bin/ -obj/ diff --git a/SkinManagerMod/CarMaterialData.cs b/SkinManagerMod/CarMaterialData.cs index 3504628..d8c5f90 100644 --- a/SkinManagerMod/CarMaterialData.cs +++ b/SkinManagerMod/CarMaterialData.cs @@ -13,10 +13,33 @@ namespace SkinManagerMod { public class CarMaterialData { - private static readonly Dictionary _liveryToMaterialsMap = new Dictionary(); + private static readonly Dictionary _liveryToMaterialsMap = new(); + private static readonly Dictionary> _materialSubstitutes = new(); public static void Initialize() { + // built map of all substituted textures + foreach (var defaultTheme in SkinProvider.BuiltInThemes) + { + if (defaultTheme.name == SkinProvider.DefaultThemeName) continue; + + BaseTheme themeType = SkinProvider.GetThemeTypeByName(defaultTheme.name); + + foreach (var substitute in defaultTheme.substitutions) + { + if (!substitute.substitute) continue; + + string originalName = GetCleanName(substitute.original); + + if (!_materialSubstitutes.TryGetValue(originalName, out var alternatives)) + { + alternatives = new List(); + _materialSubstitutes.Add(originalName, alternatives); + } + alternatives.Add(new ThemeAlternative(themeType, substitute.substitute)); + } + } + foreach (var livery in Globals.G.Types.Liveries) { var carData = new CarMaterialData(livery); @@ -26,35 +49,25 @@ public static void Initialize() public static CarMaterialData GetDataForCar(string liveryId) => _liveryToMaterialsMap[liveryId]; + private static string GetCleanName(Material material) => GetCleanName(material.name); + private static string GetCleanName(string materialName) => materialName.Replace(" (Instance)", string.Empty); public readonly string LiveryId; - public readonly CarAreaMaterialData Exterior; - public readonly CarAreaMaterialData Interior; - public CarMaterialData(TrainCarLivery livery) - { - LiveryId = livery.id; - Exterior = new CarAreaMaterialData(livery.prefab); - Interior = new CarAreaMaterialData(livery.interiorPrefab); - } - } + private readonly Dictionary _materialData; + private readonly Dictionary> _texToMaterialMap; - public readonly struct MaterialTexTypePair - { - public readonly Material Material; - public readonly string PropertyName; + public IEnumerable MaterialData => _materialData.Values; - public MaterialTexTypePair(Material material, string texType) + public IEnumerable GetAllMaterials() => MaterialData.Select(m => m.Material); + + public MaterialTextureData? GetBodyMaterial() { - Material = material; - PropertyName = texType; - } - } + var body = _materialData.Values.FirstOrDefault(m => m.CleanMaterialName.Contains("Body")); + if (body is not null) return body; - public class CarAreaMaterialData - { - private readonly List _materialData; - private readonly Dictionary> _texToMaterialMap; + return _materialData.Values.FirstOrDefault(m => m.CleanMaterialName.Contains("Car")); + } public IEnumerable GetTextureAssignments(string textureName) { @@ -65,41 +78,57 @@ public IEnumerable GetTextureAssignments(string textureName return Enumerable.Empty(); } - public CarAreaMaterialData(GameObject areaRoot) + public MaterialTextureData? GetDataForMaterial(Material material) + { + if (material && _materialData.TryGetValue(GetCleanName(material), out var data)) + { + return data; + } + return null; + } + + public bool GetDataForMaterial(string materialName, out MaterialTextureData? data) { - _materialData = new List(); + return _materialData.TryGetValue(GetCleanName(materialName), out data); + } + + public CarMaterialData(TrainCarLivery livery) + { + LiveryId = livery.id; + + _materialData = new Dictionary(); _texToMaterialMap = new Dictionary>(); - if (!areaRoot) return; + IEnumerable renderers = livery.prefab.GetComponentsInChildren(true); - foreach (var renderer in areaRoot.GetComponentsInChildren(true)) + if (livery.interiorPrefab) + { + renderers = renderers.Concat(livery.interiorPrefab.GetComponentsInChildren(true)); + } + + foreach (var renderer in renderers) { if (!renderer.sharedMaterial) continue; - var data = new MaterialTextureData(renderer.sharedMaterial); - _materialData.Add(data); + string cleanName = GetCleanName(renderer.sharedMaterial); + if (_materialData.ContainsKey(cleanName)) continue; - RegisterTextureName(data.DiffuseName, data.Material, TextureUtility.PropNames.Main); - if (data.BumpMapName != null) + IEnumerable alternatives; + if (_materialSubstitutes.TryGetValue(cleanName, out var altList)) { - RegisterTextureName(data.BumpMapName, data.Material, TextureUtility.PropNames.BumpMap); + alternatives = altList; } - if (data.SpecularOcclusionName != null) + else { - RegisterTextureName(data.SpecularOcclusionName, data.Material, TextureUtility.PropNames.MetalGlossMap); - } - if (data.EmissionMapName != null) - { - RegisterTextureName(data.EmissionMapName, data.Material, TextureUtility.PropNames.EmissionMap); + alternatives = Enumerable.Empty(); } - if (data.DetailAlbedoName != null) - { - RegisterTextureName(data.DetailAlbedoName, data.Material, TextureUtility.PropNames.DetailAlbedo); - } - if (data.DetailNormalName != null) + var data = new MaterialTextureData(renderer.sharedMaterial, alternatives); + _materialData.Add(cleanName, data); + + foreach (var texture in data.AllTextures) { - RegisterTextureName(data.DetailNormalName, data.Material, TextureUtility.PropNames.DetailNormal); + RegisterTextureName(texture.TextureName, data.Material, texture.PropertyName); } } } @@ -120,33 +149,86 @@ private void RegisterTextureName(string texName, Material material, string texTy public class MaterialTextureData { public readonly Material Material; - public readonly Material SubstitutedMaterial; - public string DiffuseName; - public string BumpMapName; - public string SpecularOcclusionName; - public string EmissionMapName; + private readonly string _cleanName; + public string CleanMaterialName => _cleanName; + + private readonly List _textures = new(TextureUtility.PropNames.AllTextures.Length); + + public List AllTextures => _textures; - public string DetailAlbedoName; - public string DetailNormalName; + private readonly List _alternatives; + + public TexturePropNamePair? GetTexture(string propName) + { + foreach (var tex in _textures) + { + if (tex.PropertyName == propName) return tex; + } + return null; + } - public MaterialTextureData(Material material) + public MaterialTextureData(Material material, IEnumerable alternatives) { Material = material; - DiffuseName = GetTexName(material, TextureUtility.PropNames.Main); - BumpMapName = GetTexName(material, TextureUtility.PropNames.BumpMap); - SpecularOcclusionName = GetTexName(material, TextureUtility.PropNames.MetalGlossMap); - EmissionMapName = GetTexName(material, TextureUtility.PropNames.EmissionMap); + _cleanName = GetCleanName(material); - DetailAlbedoName = GetTexName(material, TextureUtility.PropNames.DetailAlbedo); - DetailNormalName = GetTexName(material, TextureUtility.PropNames.DetailNormal); + foreach (string property in TextureUtility.PropNames.AllTextures) + { + if (material.HasProperty(property) && (material.GetTexture(property) is Texture tex)) + { + _textures.Add(new TexturePropNamePair(tex.name, property)); + } + } + + _alternatives = alternatives.ToList(); + } + + public Material GetMaterialForBaseTheme(BaseTheme themeType) + { + int altIndex = _alternatives.FindIndex(a => a.Theme == themeType); + if (altIndex >= 0) + { + return _alternatives[altIndex].Material; + } + return Material; } + } + + public readonly struct ThemeAlternative + { + public readonly BaseTheme Theme; + public readonly Material Material; - private static string GetTexName(Material material, string property) + public ThemeAlternative(BaseTheme theme, Material substitute) { - if (!material.HasProperty(property) || !(material.GetTexture(property) is Texture tex)) return null; - return tex.name; + Theme = theme; + Material = substitute; } } } + + public readonly struct MaterialTexTypePair + { + public readonly Material Material; + public readonly string PropertyName; + + public MaterialTexTypePair(Material material, string texType) + { + Material = material; + PropertyName = texType; + } + } + + public readonly struct TexturePropNamePair + { + public readonly string TextureName; + public readonly string PropertyName; + + public TexturePropNamePair(string texName, string propName) + { + TextureName = texName; + PropertyName = propName; + } + } } diff --git a/SkinManagerMod/CarPatches.cs b/SkinManagerMod/CarPatches.cs deleted file mode 100644 index 3b70387..0000000 --- a/SkinManagerMod/CarPatches.cs +++ /dev/null @@ -1,68 +0,0 @@ -using DV.Customization.Paint; -using HarmonyLib; -using System.Collections.Generic; -using System.Reflection; - -namespace SkinManagerMod -{ - [HarmonyPatch] - internal static class CarSpawnerPatches - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(CarSpawner), nameof(CarSpawner.SpawnCar)); - yield return AccessTools.Method(typeof(CarSpawner), nameof(CarSpawner.SpawnLoadedCar)); - } - - [HarmonyPostfix] - private static void BaseSpawn(TrainCar __result) - { - var skinName = SkinManager.GetCurrentCarSkin(__result); - if (!string.IsNullOrEmpty(skinName)) - { - // only need to replace textures if not staying with default skin - SkinManager.ApplySkin(__result, skinName); - } - } - } - - [HarmonyPatch] - class TrainCar_LoadInterior_Patch - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(TrainCar), nameof(TrainCar.LoadInterior)); - yield return AccessTools.Method(typeof(TrainCar), nameof(TrainCar.LoadExternalInteractables)); - yield return AccessTools.Method(typeof(TrainCar), nameof(TrainCar.LoadDummyExternalInteractables)); - } - - static void Postfix(TrainCar __instance) - { - if (SkinProvider.IsThemeable(__instance.carLivery)) return; - - var skinName = SkinManager.GetCurrentCarSkin(__instance); - if (!string.IsNullOrEmpty(skinName)) - { - SkinManager.ApplyNonThemeSkinToInterior(__instance, skinName); - } - } - } - - [HarmonyPatch(typeof(TrainCarPaint))] - internal static class TrainCarPaintPatch - { - [HarmonyPatch(nameof(TrainCarPaint.CurrentTheme), MethodType.Setter)] - [HarmonyPostfix] - public static void AfterCurrentThemeSet(TrainCarPaint __instance, PaintTheme ___currentTheme) - { - var trainCar = TrainCar.Resolve(__instance.gameObject); - string themeName = ___currentTheme ? ___currentTheme.name : null; - - Main.Log($"Applying skin {themeName} to car {trainCar.ID} {__instance.TargetArea}"); - - if (__instance.TargetArea != TrainCarPaint.Target.Exterior) return; - - SkinManager.SetAppliedCarSkin(trainCar, themeName); - } - } -} diff --git a/SkinManagerMod/CommsRadioSkinSwitcher.cs b/SkinManagerMod/CommsRadioSkinSwitcher.cs index 67aa3e6..1b22072 100644 --- a/SkinManagerMod/CommsRadioSkinSwitcher.cs +++ b/SkinManagerMod/CommsRadioSkinSwitcher.cs @@ -10,6 +10,7 @@ using UnityEngine; using static DV.Common.GameFeatureFlags; +#nullable disable namespace SkinManagerMod { public class CommsRadioSkinSwitcher : MonoBehaviour, ICommsRadioMode @@ -35,13 +36,16 @@ public class CommsRadioSkinSwitcher : MonoBehaviour, ICommsRadioMode private RaycastHit Hit; private TrainCar SelectedCar = null; private TrainCar PointedCar = null; + private bool HasInterior = false; private MeshRenderer HighlighterRender; + private PaintArea AreaToPaint = PaintArea.All; + private PaintArea AlreadyPainted = PaintArea.None; - private List SkinsForCarType = null; + private List SkinsForCarType = null; private int SelectedSkinIdx = 0; - private string SelectedSkin = null; - private string CurrentSkin = null; + private CustomPaintTheme SelectedSkin = null; + private (string exterior, string interior) CurrentThemeName; private const float SIGNAL_RANGE = 100f; private static readonly Vector3 HIGHLIGHT_BOUNDS_EXTENSION = new Vector3(0.25f, 0.8f, 0f); @@ -196,7 +200,7 @@ private void SetState(State newState) case State.SelectSkin: UpdateAvailableSkinsList(SelectedCar.carLivery); SetSelectedSkin(SkinsForCarType?.FirstOrDefault()); - CurrentSkin = SkinManager.GetCurrentCarSkin(SelectedCar, false); + CurrentThemeName = SkinManager.GetCurrentCarSkin(SelectedCar, false); ButtonBehaviour = ButtonBehaviourType.Override; break; @@ -252,7 +256,7 @@ public void OnUpdate() { PointToCar(trainCar); - if (SelectedSkin == CurrentSkin) + if (!HasInterior && !SkinProvider.IsBuiltInTheme(SelectedSkin) && (SelectedSkin.name == CurrentThemeName.exterior)) { display.SetAction(Translations.ReloadAction); } @@ -276,7 +280,15 @@ public void OnUpdate() (trainCar = TrainCar.Resolve(Hit.transform.root)) && (trainCar == SelectedCar)) { PointToCar(trainCar); - display.SetAction(Translations.ConfirmAction); + + if ((AlreadyPainted == AreaToPaint) && !SkinProvider.IsBuiltInTheme(SelectedSkin)) + { + display.SetAction(Translations.ReloadAction); + } + else + { + display.SetAction(Translations.ConfirmAction); + } } else { @@ -295,16 +307,13 @@ private string AreaToPaintName { get { - switch (AreaToPaint) + return AreaToPaint switch { - case PaintArea.Exterior: - return CommsRadioLocalization.MODE_PAINTJOB_EXTERIOR; - case PaintArea.Interior: - return CommsRadioLocalization.MODE_PAINTJOB_INTERIOR; - case PaintArea.All: - default: - return CommsRadioLocalization.MODE_PAINTJOB_ALL; + PaintArea.Exterior => CommsRadioLocalization.MODE_PAINTJOB_EXTERIOR, + PaintArea.Interior => CommsRadioLocalization.MODE_PAINTJOB_INTERIOR, + _ => CommsRadioLocalization.MODE_PAINTJOB_ALL, }; + ; } } @@ -316,6 +325,7 @@ public void OnUse() if (PointedCar != null) { SelectedCar = PointedCar; + HasInterior = SelectedCar.GetComponents().Any(tcp => tcp.TargetArea == TrainCarPaint.Target.Interior); PointedCar = null; HighlightCar(SelectedCar, skinningMaterial); @@ -327,24 +337,22 @@ public void OnUse() case State.SelectSkin: if ((PointedCar != null) && (PointedCar == SelectedCar)) { - // clicked on the selected car again, this means confirm - if (SelectedSkin == CurrentSkin) + if (HasInterior) { - SkinProvider.ReloadSkin(SelectedCar.carLivery.id, SelectedSkin); - ResetState(); + SetState(State.SelectAreas); } else { - if (SkinProvider.IsThemeable(PointedCar.carLivery.id)) + // for regular cars, skip area selection + if (!SkinProvider.IsBuiltInTheme(SelectedSkin) && (SelectedSkin.name == CurrentThemeName.exterior)) { - SetState(State.SelectAreas); + SkinProvider.ReloadSkin(SelectedCar.carLivery.id, SelectedSkin.name); } else { - // for regular cars, skip area selection ApplySelectedSkin(); - ResetState(); } + ResetState(); } CommsRadioController.PlayAudioFromRadio(ConfirmSound, transform); } @@ -360,7 +368,14 @@ public void OnUse() if ((PointedCar != null) && (PointedCar == SelectedCar)) { // clicked on the selected car again, this means confirm - ApplySelectedSkin(); + if ((AlreadyPainted == AreaToPaint) && !SkinProvider.IsBuiltInTheme(SelectedSkin)) + { + SkinProvider.ReloadSkin(SelectedCar.carLivery.id, SelectedSkin.name); + } + else + { + ApplySelectedSkin(); + } CommsRadioController.PlayAudioFromRadio(ConfirmSound, transform); } @@ -439,7 +454,7 @@ private void ApplySelectedSkin() } SkinManager.ApplySkin(SelectedCar, SelectedSkin, AreaToPaint); - CurrentSkin = SelectedSkin; + //CurrentThemeName = SelectedSkin.name; if (CarTypes.IsMUSteamLocomotive(SelectedCar.carType) && SelectedCar.rearCoupler.IsCoupled()) { @@ -447,7 +462,7 @@ private void ApplySelectedSkin() if ((attachedCar != null) && CarTypes.IsTender(attachedCar.carLivery)) { // car attached behind loco is tender - if (SkinProvider.IsBuiltInTheme(SelectedSkin) || !(SkinProvider.FindSkinByName(attachedCar.carLivery, SelectedSkin) is null)) + if (SelectedSkin.SupportsVehicle(attachedCar.carLivery)) { // found a matching skin for the tender :D SkinManager.ApplySkin(attachedCar, SelectedSkin, AreaToPaint); @@ -456,9 +471,9 @@ private void ApplySelectedSkin() } } - private void SetSelectedSkin(string skinName) + private void SetSelectedSkin(CustomPaintTheme theme) { - if (string.IsNullOrEmpty(skinName)) + if (theme is null) { // should never actually reach this code due to built in themes SelectedSkin = null; @@ -466,14 +481,19 @@ private void SetSelectedSkin(string skinName) } else { - SelectedSkin = skinName; + SelectedSkin = theme; - if (skinName == Skin.GetDefaultSkinName(SelectedCar.carLivery.id)) + AlreadyPainted = PaintArea.None; + if (CurrentThemeName.exterior == SelectedSkin.name) + { + AlreadyPainted |= PaintArea.Exterior; + } + if (CurrentThemeName.interior == SelectedSkin.name) { - skinName = SkinProvider.DefaultThemeName; + AlreadyPainted |= PaintArea.Interior; } - string displayName = $"{Translations.SelectPaintPrompt}\n{skinName}"; + string displayName = $"{Translations.SelectPaintPrompt}\n{SelectedSkin.LocalizedName}"; display.SetContent(displayName); } } diff --git a/SkinManagerMod/CustomPaintTheme.cs b/SkinManagerMod/CustomPaintTheme.cs new file mode 100644 index 0000000..2f89c89 --- /dev/null +++ b/SkinManagerMod/CustomPaintTheme.cs @@ -0,0 +1,72 @@ +using DV; +using DV.Customization.Paint; +using DV.ThingTypes; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace SkinManagerMod +{ + public class CustomPaintTheme : PaintTheme + { + private Dictionary _skins = new(); + + public void AddSkin(Skin skin) => _skins.Add(skin.LiveryId, skin); + + public void RemoveSkin(Skin skin) => _skins.Remove(skin.LiveryId); + public void RemoveSkin(string liveryId) => _skins.Remove(liveryId); + + public bool SupportsVehicle(string liveryId) => _skins.ContainsKey(liveryId); + public bool SupportsVehicle(TrainCarLivery livery) => _skins.ContainsKey(livery.id); + + public IEnumerable SupportedCarTypes => Globals.G.Types.Liveries.Where(type => _skins.ContainsKey(type.id)); + + public void Apply(GameObject target, TrainCar train) + { + if (_skins.TryGetValue(train.carLivery.id, out var skin)) + { + var defaultSkin = CarMaterialData.GetDataForCar(train.carLivery.id); + + ApplyToTransform(target, skin, defaultSkin); + } + } + + private static void ApplyToTransform(GameObject objectRoot, Skin skin, CarMaterialData defaults) + { + foreach (var renderer in objectRoot.GetComponentsInChildren(true)) + { + if (!renderer.material) + { + continue; + } + + TextureUtility.ApplyTextures(renderer, skin, defaults); + } + } + + public Texture2D? GetBodyTexture() + { + return _skins.Values.Select(GetBodyTexture) + .Where(t => t) + .OrderBy(t => t!.name) + .FirstOrDefault(); + } + + private static Texture2D? GetBodyTexture(Skin skin) + { + var materialData = CarMaterialData.GetDataForCar(skin.LiveryId); + if ((materialData.GetBodyMaterial() is CarMaterialData.MaterialTextureData bodyMaterial) && + (bodyMaterial.GetTexture(TextureUtility.PropNames.Main) is TexturePropNamePair mainTex)) + { + if (skin.GetTexture(mainTex.TextureName) is SkinTexture substitute) + { + return substitute.TextureData; + } + + return (Texture2D)bodyMaterial.Material.GetTexture(TextureUtility.PropNames.Main); + } + + return null; + } + } +} diff --git a/SkinManagerMod/DDSUtils.cs b/SkinManagerMod/DDSUtils.cs new file mode 100644 index 0000000..2f15818 --- /dev/null +++ b/SkinManagerMod/DDSUtils.cs @@ -0,0 +1,248 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace SkinManagerMod +{ + internal static class DDSUtils + { + private static int Mipmap0SizeInBytes(int width, int height, TextureFormat textureFormat) + { + var blockWidth = (width + 3) / 4; + var blockHeight = (height + 3) / 4; + var bytesPerBlock = textureFormat switch + { + TextureFormat.DXT1 => 8, + TextureFormat.DXT5 or TextureFormat.BC5 => 16, + _ => throw new ArgumentException($"Unsupported TextureFormat {textureFormat}", "textureFormat"), + }; + return blockWidth * blockHeight * bytesPerBlock; + } + + private const int DDS_HEADER_SIZE = 128; + private const int DDS_HEADER_DXT10_SIZE = 20; + private static byte[] DDSHeader(int width, int height, TextureFormat textureFormat, int numMipmaps) + { + var needsDXGIHeader = textureFormat != TextureFormat.DXT1 && textureFormat != TextureFormat.DXT5; + var headerSize = needsDXGIHeader ? DDS_HEADER_SIZE + DDS_HEADER_DXT10_SIZE : DDS_HEADER_SIZE; + var header = new byte[headerSize]; + using (var stream = new MemoryStream(header)) + { + stream.Write(Encoding.ASCII.GetBytes("DDS "), 0, 4); + stream.Write(BitConverter.GetBytes(124), 0, 4); // dwSize + // dwFlags = CAPS | HEIGHT | WIDTH | PIXELFORMAT | MIPMAPCOUNT | LINEARSIZE + stream.Write(BitConverter.GetBytes(0x1 | 0x2 | 0x4 | 0x1000 | 0x20000 | 0x80000), 0, 4); + stream.Write(BitConverter.GetBytes(height), 0, 4); + stream.Write(BitConverter.GetBytes(width), 0, 4); + stream.Write(BitConverter.GetBytes(Mipmap0SizeInBytes(width, height, textureFormat)), 0, 4); // dwPitchOrLinearSize + stream.Write(BitConverter.GetBytes(0), 0, 4); // dwDepth + stream.Write(BitConverter.GetBytes(numMipmaps), 0, 4); // dwMipMapCount + for (int i = 0; i < 11; i++) + stream.Write(BitConverter.GetBytes(0), 0, 4); // dwReserved1 + var pixelFormat = PixelFormat(textureFormat); + stream.Write(pixelFormat, 0, pixelFormat.Length); + // dwCaps = COMPLEX | MIPMAP | TEXTURE + stream.Write(BitConverter.GetBytes(0x401008), 0, 4); + // dwCaps2, dwCaps3, dwCaps4, dwReserved2 + for (int i = 0; i < 4; i++) + stream.Write(BitConverter.GetBytes(0), 0, 4); + + if (needsDXGIHeader) + stream.Write(DDSHeaderDXT10(textureFormat), 0, DDS_HEADER_DXT10_SIZE); + } + return header; + } + + private static byte[] PixelFormat(TextureFormat textureFormat) + { + string fourCC = textureFormat switch + { + TextureFormat.DXT1 => "DXT1", + TextureFormat.DXT5 => "DXT5", + _ => "DX10", + }; + + var pixelFormat = new byte[32]; + using (var stream = new MemoryStream(pixelFormat)) + { + stream.Write(BitConverter.GetBytes(32), 0, 4); // dwSize + stream.Write(BitConverter.GetBytes(0x4), 0, 4); // dwFlags = FOURCC + stream.Write(Encoding.ASCII.GetBytes(fourCC), 0, 4); // dwFourCC + } + return pixelFormat; + } + + private static int DXGIFormat(TextureFormat textureFormat) + { + return textureFormat switch + { + TextureFormat.BC5 => 83, + _ => throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"), + }; + } + + private static byte[] DDSHeaderDXT10(TextureFormat textureFormat) + { + var headerDXT10 = new byte[DDS_HEADER_DXT10_SIZE]; + using (var stream = new MemoryStream(headerDXT10)) + { + stream.Write(BitConverter.GetBytes(DXGIFormat(textureFormat)), 0, 4); // dxgiFormat + stream.Write(BitConverter.GetBytes(3), 0, 4); // resourceDimension = 3 = DDS_DIMENSION_TEXTURE2D + stream.Write(BitConverter.GetBytes(0), 0, 4); // miscFlag + stream.Write(BitConverter.GetBytes(1), 0, 4); // arraySize = 1 + stream.Write(BitConverter.GetBytes(0), 0, 4); // miscFlags2 = 0 = DDS_ALPHA_MODE_UNKNOWN + } + return headerDXT10; + } + + public static void WriteDDSGz(FileInfo fileInfo, Texture2D texture) + { + Main.Log($"Writing to {fileInfo.FullName}"); + + using var fileStream = fileInfo.OpenWrite(); + using var outfile = new GZipStream(fileStream, CompressionLevel.Optimal); + + var header = DDSHeader(texture.width, texture.height, texture.format, texture.mipmapCount); + outfile.Write(header, 0, header.Length); + + var data = texture.GetRawTextureData().ToArray(); + outfile.Write(data, 0, data.Length); + } + + private static Texture2D ReadDDSHeader(Stream infile, bool linear) + { + var buf = new byte[4096]; + var bytesRead = infile.Read(buf, 0, DDS_HEADER_SIZE); + if (bytesRead != 128 || Encoding.ASCII.GetString(buf, 0, 4) != "DDS ") + throw new DDSReadException("File is not a DDS file"); + + int height = BitConverter.ToInt32(buf, 12); + int width = BitConverter.ToInt32(buf, 16); + + int pixelFormatFlags = BitConverter.ToInt32(buf, 80); + if ((pixelFormatFlags & 0x4) == 0) + throw new DDSReadException("DDS header does not have a FourCC"); + string fourCC = Encoding.ASCII.GetString(buf, 84, 4); + TextureFormat textureFormat; + switch (fourCC) + { + case "DXT1": + textureFormat = TextureFormat.DXT1; + break; + case "DXT5": + textureFormat = TextureFormat.DXT5; + break; + case "DX10": + // read DDS_HEADER_DXT10 header extension + bytesRead = infile.Read(buf, 0, DDS_HEADER_DXT10_SIZE); + if (bytesRead != DDS_HEADER_DXT10_SIZE) + throw new DDSReadException("Could not read DXT10 header from DDS file"); + int dxgiFormat = BitConverter.ToInt32(buf, 0); + textureFormat = dxgiFormat switch + { + 83 => TextureFormat.BC5, + _ => throw new DDSReadException($"Unsupported DXGI_FORMAT {dxgiFormat}"), + }; + break; + default: + throw new DDSReadException($"Unknown FourCC: {fourCC}"); + } + + var texture = new Texture2D(width, height, textureFormat, true, linear); + return texture; + } + + public static Task ReadDDSGz(FileInfo fileInfo, bool isNormalMap) + { + FileStream? fileStream = null; + GZipStream? zipStream = null; + try + { + fileStream = fileInfo.OpenRead(); + zipStream = new GZipStream(fileStream, CompressionMode.Decompress); + + var texture = ReadDDSHeader(zipStream, isNormalMap); + if (isNormalMap && texture.format != TextureFormat.BC5) + { + Main.LogVerbose($"Cached normal map texture {fileInfo.FullName} has old format {texture.format}"); + zipStream.Close(); + fileStream.Close(); + File.Delete(fileInfo.FullName); + return Task.FromResult(null); + } + + Main.LogVerbose($"Reading cached {texture.format} texture from {fileInfo.FullName}"); + + var loader = new CacheReader(fileInfo.FullName, texture, fileStream, zipStream); + return loader.Dispatch(); + } + catch (Exception ex) + { + zipStream?.Close(); + fileStream?.Close(); + throw ex; + } + } + + internal class CacheReader : IDisposable + { + public readonly string FileName; + private readonly Texture2D _texture; + private readonly FileStream _fileStream; + private readonly GZipStream _zipstream; + + public CacheReader(string fileName, Texture2D target, FileStream fileStream, GZipStream zipStream) + { + FileName = fileName; + _texture = target; + _fileStream = fileStream; + _zipstream = zipStream; + } + + public Task Dispatch() + { + return Task.Run(DoWork); + } + + public void Dispose() + { + _zipstream?.Dispose(); + _fileStream?.Dispose(); + } + + private Texture2D? DoWork() + { + try + { + var nativeArray = _texture.GetRawTextureData(); + var buf = new byte[nativeArray.Length]; + var bytesRead = _zipstream.Read(buf, 0, nativeArray.Length); + if (bytesRead < nativeArray.Length) + { + Main.Error($"{FileName}: Expected {nativeArray.Length} bytes, but file contained {bytesRead}"); + return null; + } + nativeArray.CopyFrom(buf); + return _texture; + } + catch (Exception ex) + { + Main.Error($"Error while reading cache file {FileName}: {ex.Message}"); + return null; + } + finally + { + Dispose(); + } + } + } + } + + internal class DDSReadException : Exception + { + public DDSReadException(string message) : base(message) { } + } +} \ No newline at end of file diff --git a/SkinManagerMod/DefaultTextures.cs b/SkinManagerMod/DefaultTextures.cs new file mode 100644 index 0000000..91a126f --- /dev/null +++ b/SkinManagerMod/DefaultTextures.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SkinManagerMod +{ + public static class DefaultTextures + { + private static readonly Dictionary _bodyTextures = new() + { + { "AutorackBlue", "CarAutorackBlue_01d" }, + { "AutorackGreen", "CarAutorackGreen_01d" }, + { "AutorackRed", "CarAutorackRed_01d" }, + { "AutorackYellow", "CarAutorackYellow_01d" }, + + { "BoxcarBrown", "CarBoxcar_Brown_01d" }, + { "BoxcarGreen", "CarBoxcar_Green_01d" }, + { "BoxcarPink", "CarBoxcar_Pink_01d" }, + { "BoxcarRed", "CarBoxcar_Red_01d" }, + { "BoxcarMilitary", "CarBoxcarMilitary_01d" }, + { "RefrigeratorWhite", "CarRefrigerator_d" }, + { "StockBrown", "CarStockcar_Brown_d" }, + { "StockGreen", "CarStockcar_Green_d" }, + { "StockRed", "CarStockcar_Red_d" }, + + { "FlatbedEmpty", "CarFlatcarCBBulkheadStakes_Brown_d" }, + { "FlatbedStakes", "CarFlatcarCBBulkheadStakes_Brown_d" }, + { "FlatbedShort", "CarFlatcarShort_01d" }, + { "FlatbedMilitary", "CarFlatcarCBBulkheadStakes_Military_d" }, + + { "GondolaGray", "CarGondolaGrey_d" }, + { "GondolaGreen", "CarGondolaGreen_d" }, + { "GondolaRed", "CarGondolaRed_d" }, + + { "HopperBrown", "CarHopperBrown_d" }, + { "HopperTeal", "CarHopperTeal_d" }, + { "HopperYellow", "CarHopperYellow_d" }, + { "HopperCoveredBrown", "CarHopperCovered_01d" }, + + { "TankBlack", "CarTankBlack_01d" }, + { "TankBlue", "CarTankBlue_01d" }, + { "TankChrome", "CarTankChrome_01d" }, + { "TankOrange", "CarTankOrange_01" }, + { "TankWhite", "CarTankWhite_01d" }, + { "TankYellow", "CarTankYellow_01d" }, + { "TankShortMilk", "CarTanker_WhiteMilk_d" }, + + { "LocoDE2", "LocoDE2_Body_01d" }, + { "LocoDE6", "LocoDE6_Body_01d" }, + { "LocoDE6Slug", "LocoDE6_Body_01d" }, + { "LocoDH4", "LocoDH4_Body_01d" }, + { "LocoDM1U", "LocoDM1U-150_Body_01d" }, + { "LocoDM3", "LocoDM3_Body_01d" }, + { "LocoMicroshunter", "LocoMicroshunter_Body_01d" }, + { "LocoS060", "LocoS060_Body_01d" }, + { "LocoS282A", "LocoS282A_Body_01d" }, + { "LocoS282B", "LocoS282B_Body_01d" }, + + { "CabooseRed", "CarCabooseRed_Body_01d" }, + { "HandCar", "LocoHandcar_01d" }, + { "NuclearFlask", "CarNuclearFlask_d" }, + + { "PassengerBlue", "CarPassengerBlue_01d" }, + { "PassengerGreen", "CarPassengerGreen_01d" }, + { "PassengerRed", "CarPassengerRed_01d" }, + }; + + public static string GetBodyTextureName(string liveryId) + { + return _bodyTextures[liveryId]; + } + } +} diff --git a/SkinManagerMod/Items/CustomPaintInventorySpec.cs b/SkinManagerMod/Items/CustomPaintInventorySpec.cs index 1b7584b..463b3eb 100644 --- a/SkinManagerMod/Items/CustomPaintInventorySpec.cs +++ b/SkinManagerMod/Items/CustomPaintInventorySpec.cs @@ -6,7 +6,7 @@ namespace SkinManagerMod.Items { public class CustomPaintInventorySpec : InventoryItemSpec { - public PaintTheme Theme; + public PaintTheme Theme = null!; public static CustomPaintInventorySpec Create(InventoryItemSpec original, GameObject holder, PaintTheme theme) { diff --git a/SkinManagerMod/Items/InventoryPatches.cs b/SkinManagerMod/Items/InventoryPatches.cs deleted file mode 100644 index 85344e4..0000000 --- a/SkinManagerMod/Items/InventoryPatches.cs +++ /dev/null @@ -1,66 +0,0 @@ -using DV.CabControls; -using HarmonyLib; -using Newtonsoft.Json.Linq; -using SMShared; -using System; -using System.Collections.Generic; -using System.Reflection.Emit; -using UnityEngine; - -namespace SkinManagerMod.Items -{ - [HarmonyPatch] - internal static class InventoryPatches - { - #region On Load - - [HarmonyPatch(typeof(StartingItemsController), nameof(StartingItemsController.InstantiateItem))] - [HarmonyTranspiler] - private static IEnumerable TranspileInstantiateItem(IEnumerable instructions) - { - // replace Instantiate call - - CodeInstruction previous = null; - foreach (var instruction in instructions) - { - if (instruction.opcode == OpCodes.Stloc_3) - { - Main.LogVerbose("Patched instantiate item"); - yield return new CodeInstruction(OpCodes.Ldarg_1); // push itemData - yield return CodeInstruction.Call(typeof(InventoryPatches), nameof(CustomInstantiateItem)); - previous = instruction; - } - else - { - if (previous != null) yield return previous; - previous = instruction; - } - } - - if (previous != null) yield return previous; - } - - private static GameObject CustomInstantiateItem(GameObject prefab, Vector3 position, Quaternion rotation, StorageItemData itemData) - { - if ((itemData?.state != null) && itemData.state.TryGetValue(Constants.CUSTOM_THEME_SAVEDATA_KEY, out JToken themeToken)) - { - if ((string)themeToken is string themeName) - { - if (SkinProvider.TryGetTheme(themeName, out var theme)) - { - Main.Log($"Creating saved custom paint can for theme {themeName}"); - return PaintFactory.InstantiateCustomCan(theme, position, rotation); - } - else - { - Main.Error($"Couldn't find theme {themeName} for custom paint can"); - } - } - } - - return UnityEngine.Object.Instantiate(prefab, position, rotation); - } - - #endregion - } -} diff --git a/SkinManagerMod/Items/PaintCanPatches.cs b/SkinManagerMod/Items/PaintCanPatches.cs deleted file mode 100644 index 813c8b5..0000000 --- a/SkinManagerMod/Items/PaintCanPatches.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DV.Interaction; -using HarmonyLib; - -namespace SkinManagerMod.Items -{ - [HarmonyPatch(typeof(PaintCan))] - internal static class PaintCanPatches - { - [HarmonyPatch(nameof(PaintCan.CheckPaintApplicationValidity))] - [HarmonyPrefix] - public static void CheckPaintValidity(ref bool isCareerMode) - { - if (isCareerMode && Main.Settings.allowPaintingUnowned) - { - isCareerMode = false; - } - } - } -} diff --git a/SkinManagerMod/Items/PaintCanThemeNameProvider.cs b/SkinManagerMod/Items/PaintCanThemeNameProvider.cs index 8434d09..0d26b55 100644 --- a/SkinManagerMod/Items/PaintCanThemeNameProvider.cs +++ b/SkinManagerMod/Items/PaintCanThemeNameProvider.cs @@ -4,6 +4,7 @@ using SMShared; using UnityEngine; +#nullable disable namespace SkinManagerMod.Items { public class PaintCanThemeNameProvider : MonoBehaviour, IInventoryItemLocalizer diff --git a/SkinManagerMod/Items/PaintFactory.cs b/SkinManagerMod/Items/PaintFactory.cs index 96e3c40..d29e558 100644 --- a/SkinManagerMod/Items/PaintFactory.cs +++ b/SkinManagerMod/Items/PaintFactory.cs @@ -1,15 +1,13 @@ -using DV; -using DV.CabControls; +using DV.CabControls; using DV.Customization.Paint; using DV.Interaction; using DV.Localization; using DV.Shops; -using DV.ThingTypes; using OokiiTsuki.Palette; -using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using TMPro; using UnityEngine; @@ -19,22 +17,34 @@ public static class PaintFactory { public const string DEFAULT_CAN_PREFAB_NAME = "PaintCan"; - public static string GetDummyPrefabName(string themeName) => $"SM_ItemSpec_{themeName}"; + public const string DUMMY_CAN_PREFIX = "SM_ItemSpec"; + public static string GetDummyPrefabName(string themeName) => $"{DUMMY_CAN_PREFIX}_{themeName}"; - private static GameObject _defaultCanPrefab = null; + private static Regex _dummyCanRegex = new($"^{DUMMY_CAN_PREFIX}_(.+)$"); + + public static bool TryParseDummyPrefabName(string prefabName, out string? themeName) + { + var match = _dummyCanRegex.Match(prefabName); + if (match.Success) + { + themeName = match.Groups[1].Value; + return true; + } + themeName = null; + return false; + } + + private static GameObject? _defaultCanPrefab = null; public static GameObject DefaultCanPrefab { get { - if (_defaultCanPrefab is null) - { - _defaultCanPrefab = Resources.Load("PaintCan") as GameObject; - } - return _defaultCanPrefab; + _defaultCanPrefab ??= Resources.Load("PaintCan") as GameObject; + return _defaultCanPrefab!; } } - private static Material _defaultCanLabelMaterial = null; + private static Material? _defaultCanLabelMaterial = null; public static Material DefaultCanLabelMaterial { get @@ -48,26 +58,23 @@ public static Material DefaultCanLabelMaterial } } - private static ShopItemData _defaultCanShopData = null; + private static ShopItemData? _defaultCanShopData = null; private static ShopItemData DefaultCanShopData { get { - if (_defaultCanShopData is null) - { - _defaultCanShopData = GlobalShopController.Instance.GetShopItemData(DEFAULT_CAN_PREFAB_NAME); - } + _defaultCanShopData ??= GlobalShopController.Instance.GetShopItemData(DEFAULT_CAN_PREFAB_NAME); return _defaultCanShopData; } } - private static readonly Dictionary _labelMaterials = new Dictionary(); + private static readonly Dictionary _labelMaterials = new(); - private static GameObject _labelTextGizmo; - private static TextMeshProUGUI _themeNameTextMesh; - private static TextMeshProUGUI _carTypesTextMesh; - private static TMP_FontAsset _labelFont; - private static Camera _textCamera; + private static GameObject? _labelTextGizmo; + private static TextMeshProUGUI? _themeNameTextMesh; + private static TextMeshProUGUI? _carTypesTextMesh; + private static TMP_FontAsset? _labelFont; + private static Camera? _textCamera; private static readonly Texture2D _labelBackgroundTexture; private static readonly Texture2D _labelAccentBottom; @@ -119,9 +126,12 @@ static PaintFactory() public static GameObject InstantiateCustomCan(PaintTheme theme, Vector3 position, Quaternion rotation) { + string prefabName = GetDummyPrefabName(theme.name); + var newCan = UnityEngine.Object.Instantiate(DefaultCanPrefab, position, rotation); var itemSpec = newCan.GetComponent(); + itemSpec.itemPrefabName = prefabName; itemSpec.localizationKeyName = Translations.PaintCanNameKey; itemSpec.ItemIconSprite = _inventoryIcon; itemSpec.ItemIconSpriteDropped = _droppedIcon; @@ -136,10 +146,11 @@ public static GameObject InstantiateCustomCan(PaintTheme theme, Vector3 position nameProvider.theme = theme; var restocker = newCan.GetComponent(); - restocker.itemPrefabName = GetDummyPrefabName(theme.name); + restocker.itemPrefabName = prefabName; var paintSpec = newCan.GetComponent(); paintSpec.theme = theme; + paintSpec.transitionsFrom = new PaintTheme[] { SkinProvider.PrimerTheme }; return newCan; } @@ -200,13 +211,13 @@ public static void ApplyLabelMaterialToShelfItem(GameObject shelfItem, string th } } - private static void GenerateDefaultLabelMaterial(Material labelMaterial, string themeName, ThemeSettings themeSettings = null) + private static void GenerateDefaultLabelMaterial(Material labelMaterial, string themeName, ThemeSettings? themeSettings = null) { SkinProvider.TryGetTheme(themeName, out var theme); Color baseColor, accentA, accentB; - if (themeSettings != null) + if (themeSettings is not null) { // use user supplied colors baseColor = themeSettings.LabelBaseColor ?? Color.white; @@ -216,19 +227,16 @@ private static void GenerateDefaultLabelMaterial(Material labelMaterial, string else { // calculate label colors from texture palette - var bodySubstitution = theme.substitutions.OrderBy(s => s.original.name) - .FirstOrDefault(s => s.original.name.Contains("Body")); + var bodySubstitution = theme.GetBodyTexture(); - if (!bodySubstitution.original) + if (!bodySubstitution) { - Main.Error($"No body sub for theme {themeName}"); + Main.Warning($"No body sub for theme {themeName}"); labelMaterial.mainTexture = Texture2D.whiteTexture; return; } - var palette = Palette.Generate((Texture2D)bodySubstitution.substitute.mainTexture, 12); - - //WritePaletteBmp(palette, themeName); + var palette = Palette.Generate(bodySubstitution!, 12); var byPopulation = palette.mSwatches.OrderByDescending(s => s.Population).ToList(); baseColor = byPopulation.First().ToColor(); @@ -251,14 +259,14 @@ private static void GenerateDefaultLabelMaterial(Material labelMaterial, string BlitLabelSection(_labelAccentTop, blitTarget, accentB); if (!_labelTextGizmo) CreateTextGizmo(); - _labelTextGizmo.SetActive(true); + _labelTextGizmo!.SetActive(true); - _themeNameTextMesh.color = GetOverlayTextColor(accentB); + _themeNameTextMesh!.color = GetOverlayTextColor(accentB); _themeNameTextMesh.text = theme.LocalizedName; - _carTypesTextMesh.text = GetCarTypesText(themeName); + _carTypesTextMesh!.text = GetCarTypesText(theme); - _textCamera.targetTexture = blitTarget; + _textCamera!.targetTexture = blitTarget; _textCamera.Render(); _labelTextGizmo.SetActive(false); @@ -325,38 +333,16 @@ private static Color GetOverlayTextColor(Color background) return (intensity > _whiteBlackTextThreshold) ? Color.black : _whiteTextColor; } - private static string GetCarTypesText(string themeName) + private static string GetCarTypesText(CustomPaintTheme theme) { - var carNames = SkinProvider.ThemeableSkinGroups - .Where(g => g.Skins.Any(s => s.Name == themeName)) - .Select(g => LocalizationAPI.L(g.TrainCarType.localizationKey)); + var carNames = theme.SupportedCarTypes + .Select(l => l.parentType) + .Distinct() + .Select(type => LocalizationAPI.L(type.localizationKey)); return string.Join("\n", carNames); } - private static void WritePaletteBmp(Palette palette, string themeName) - { - var tex = new Texture2D(palette.mSwatches.Count, 2, TextureFormat.ARGB32, false); - - var byPopulation = palette.mSwatches - .OrderByDescending(s => s.Population); - - var mostUsedColor = byPopulation.First().ToColor(); - - var pixels = palette.mSwatches - .OrderByDescending(s => s.Population * s.ToColor().CalculateContrast(mostUsedColor)) - .Select(s => s.ToColor()); - - pixels = byPopulation - .Select(s => s.ToColor()) - .Concat(pixels); - - tex.SetPixels(pixels.ToArray()); - tex.Apply(); - - TextureUtility.SaveTextureAsPNG(tex, Path.Combine(Main.ExportFolderPath, "_Palette", $"{themeName}_palette.png")); - } - public static Palette GenPalette(string themeName) { SkinProvider.TryGetTheme(themeName, out var theme); @@ -443,8 +429,8 @@ private static void CreateTextGizmo() } public static bool ShopDataInjected { get; private set; } = false; - private static GameObject _dummyItemSpecHolder; - private static readonly Dictionary _dummyItemSpecs = new Dictionary(); + private static GameObject? _dummyItemSpecHolder; + private static readonly Dictionary _dummyItemSpecs = new(); public static CustomPaintInventorySpec GetDummyItemSpec(string themeName) => _dummyItemSpecs[themeName]; @@ -453,15 +439,27 @@ public static void InjectShopData() if (ShopDataInjected) return; Main.Log("Injecting global shop data"); + if (Object.FindObjectOfType() is DisplayLoadingInfo info) + { + const float LS_SATURATION = 0.7f; + const float LS_VALUE = 0.8f; + var color = (Color32)Random.ColorHSV(0.01f, 0.9f, LS_SATURATION, LS_SATURATION, LS_VALUE, LS_VALUE); + + string colString = $"{color.r:X2}{color.g:X2}{color.b:X2}"; + + info.loadProgressTMP.richText = true; + info.loadProgressTMP.text += $"\n{Translations.LoadingScreen}"; + } + _dummyItemSpecHolder = new GameObject("[SM] Dummy Item Holder"); + var canFab = Resources.Load(DEFAULT_CAN_PREFAB_NAME); - foreach (var theme in SkinProvider.PaintThemes.Where(SkinProvider.IsThemeAllowedInStore)) + foreach (var theme in SkinProvider.ModdedThemes.Where(SkinProvider.IsThemeAllowedInStore)) { var subHolder = new GameObject($"[SM] Dummy Item Spec {theme.name}"); subHolder.transform.SetParent(_dummyItemSpecHolder.transform, false); // dummy item spec - var canFab = Resources.Load(DEFAULT_CAN_PREFAB_NAME); var originalItemSpec = canFab.GetComponent(); var newItemSpec = CustomPaintInventorySpec.Create(originalItemSpec, subHolder, theme); @@ -499,7 +497,7 @@ public static void DestroyInjectedShopData() Main.Log("Destroying global shop data"); _dummyItemSpecs.Clear(); - UnityEngine.Object.Destroy(_dummyItemSpecHolder); + Object.Destroy(_dummyItemSpecHolder); ShopDataInjected = false; } diff --git a/SkinManagerMod/Items/ShopPaintCanStocker.cs b/SkinManagerMod/Items/ShopPaintCanStocker.cs index cb951b1..711e5d6 100644 --- a/SkinManagerMod/Items/ShopPaintCanStocker.cs +++ b/SkinManagerMod/Items/ShopPaintCanStocker.cs @@ -6,6 +6,7 @@ using System.Linq; using UnityEngine; +#nullable disable namespace SkinManagerMod.Items { public class ShopPaintCanStocker : MonoBehaviour @@ -16,7 +17,7 @@ public class ShopPaintCanStocker : MonoBehaviour public CashRegisterWithModules CashRegister; private GameObject _scanModulePrototype; - private readonly List _injectedModules = new List(NUM_THEMES_TO_STOCK); + private readonly List _injectedModules = new(NUM_THEMES_TO_STOCK); private ScanItemCashRegisterModule[] _preInjectionModules = null; public void Awake() diff --git a/SkinManagerMod/Main.cs b/SkinManagerMod/Main.cs index 011a25a..7b399af 100644 --- a/SkinManagerMod/Main.cs +++ b/SkinManagerMod/Main.cs @@ -16,9 +16,11 @@ namespace SkinManagerMod { public static class Main { +#nullable disable public static UnityModManager.ModEntry Instance { get; private set; } public static SkinManagerSettings Settings { get; private set; } public static TranslationInjector TranslationInjector { get; private set; } +#nullable restore public static string ExportFolderPath => Path.Combine(Instance.Path, Constants.EXPORT_FOLDER_NAME); public static string GetExportFolderForCar(string carId) @@ -37,6 +39,7 @@ public static bool Load(UnityModManager.ModEntry modEntry) TranslationInjector.AddTranslationsFromCsv(Path.Combine(Instance.Path, "translations.csv")); TranslationInjector.AddTranslationsFromWebCsv("https://docs.google.com/spreadsheets/d/1TrI4RuUgCijOuCjxM_WsOO9AV0BO4noTIZIzal3HbnY/export?format=csv&gid=1691364666"); + SkinProvider.CacheDefaultThemes(); CarMaterialData.Initialize(); if (!SkinProvider.Initialize()) { @@ -61,10 +64,10 @@ public static bool Load(UnityModManager.ModEntry modEntry) #region Settings static Vector2 scrollViewVector = Vector2.zero; - static TrainCarLivery trainCarSelected = null; + static TrainCarLivery? trainCarSelected = null; static bool showDropdown = false; - private static string _guiMessage; + private static string? _guiMessage; static void OnGUI(UnityModManager.ModEntry modEntry) { @@ -90,6 +93,7 @@ static void OnGUI(UnityModManager.ModEntry modEntry) Translations.DefaultSkinMode.PreferReskins, Translations.DefaultSkinMode.AllowForCustomCars, Translations.DefaultSkinMode.AllowForAllCars, + Translations.DefaultSkinMode.PreferDefaults, }; Settings.defaultSkinsMode = (DefaultSkinsMode)GUILayout.SelectionGrid((int)Settings.defaultSkinsMode, defaultSkinModeTexts, 1, "toggle"); @@ -165,7 +169,7 @@ static void OnGUI(UnityModManager.ModEntry modEntry) GUILayout.EndVertical(); } - private static Coroutine _exportAllCoro = null; + private static Coroutine? _exportAllCoro = null; private static int _completedLiveryCount = 0; private static int _totalLiveryCount = 0; @@ -231,7 +235,8 @@ public enum DefaultSkinsMode { PreferReplacements, AllowForCustomCars, - AllowForAllCars + AllowForAllCars, + PreferDefaults, } public class SkinManagerSettings : UnityModManager.ModSettings diff --git a/SkinManagerMod/Palette/Palette.cs b/SkinManagerMod/Palette/Palette.cs index 1570c55..d4b0229 100644 --- a/SkinManagerMod/Palette/Palette.cs +++ b/SkinManagerMod/Palette/Palette.cs @@ -24,12 +24,12 @@ public class Palette public List mSwatches; private int mHighestPopulation; - public Swatch VibrantSwatch { get; private set; } - public Swatch MutedSwatch { get; private set; } - public Swatch DarkVibrantSwatch { get; private set; } - public Swatch DarkMutedSwatch { get; private set; } - public Swatch LightVibrantSwatch { get; private set; } - public Swatch LightMutedSwatch { get; private set; } + public Swatch? VibrantSwatch { get; private set; } + public Swatch? MutedSwatch { get; private set; } + public Swatch? DarkVibrantSwatch { get; private set; } + public Swatch? DarkMutedSwatch { get; private set; } + public Swatch? LightVibrantSwatch { get; private set; } + public Swatch? LightMutedSwatch { get; private set; } public static Palette Generate(Texture2D texture, int numColors = DEFAULT_CALCULATE_NUMBER_COLORS) { @@ -70,10 +70,10 @@ private bool IsAlreadySelected(Swatch swatch) LightVibrantSwatch == swatch || MutedSwatch == swatch || DarkMutedSwatch == swatch || LightMutedSwatch == swatch; } - private Swatch FindColor(float targetLuma, float minLuma, float maxLuma, + private Swatch? FindColor(float targetLuma, float minLuma, float maxLuma, float targetSaturation, float minSaturation, float maxSaturation) { - Swatch max = null; + Swatch? max = null; float maxValue = 0f; foreach (Swatch swatch in mSwatches) { @@ -218,7 +218,8 @@ public class Swatch public int Blue { get; private set; } public int Rgb { get; private set; } public int Population { get; private set; } - private float[] mHsl; + private float[]? mHsl; + public Swatch(int rgbColor, int population) { Red = rgbColor.Red(); @@ -250,9 +251,8 @@ public float[] Hsl { get { - if (mHsl == null) - // Lazily generate HSL values from RGB - mHsl = ColorUtils.RGBtoHSL(Red, Green, Blue); + // Lazily generate HSL values from RGB + mHsl ??= ColorUtils.RGBtoHSL(Red, Green, Blue); return mHsl; } } diff --git a/SkinManagerMod/Patches/CarPatches.cs b/SkinManagerMod/Patches/CarPatches.cs new file mode 100644 index 0000000..9464b33 --- /dev/null +++ b/SkinManagerMod/Patches/CarPatches.cs @@ -0,0 +1,80 @@ +using DV.Customization.Paint; +using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace SkinManagerMod.Patches +{ + [HarmonyPatch] + internal static class CarSpawnerPatches + { + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(CarSpawner), nameof(CarSpawner.SpawnCar)); + yield return AccessTools.Method(typeof(CarSpawner), nameof(CarSpawner.SpawnLoadedCar)); + } + + [HarmonyPostfix] + private static void BaseSpawn(TrainCar __result, bool uniqueCar) + { + if (!__result.PaintExterior) + { + __result.gameObject.SetActive(false); + + var paintExt = __result.gameObject.AddComponent(); + paintExt.targetArea = TrainCarPaint.Target.Exterior; + paintExt.currentTheme = SkinProvider.CustomDefaultTheme; + __result.PaintExterior = paintExt; + + if (__result.carLivery.interiorPrefab) + { + var paintInt = __result.gameObject.AddComponent(); + paintInt.targetArea = TrainCarPaint.Target.Interior; + paintInt.currentTheme = SkinProvider.CustomDefaultTheme; + __result.PaintInterior = paintInt; + } + + __result.gameObject.SetActive(true); + } + + if (!uniqueCar) + { + (string? exterior, string? interior) = SkinManager.GetCurrentCarSkin(__result); + + if (__result.PaintInterior && interior is not null && SkinProvider.TryGetTheme(interior, out var interiorTheme)) + { + __result.PaintInterior.CurrentTheme = interiorTheme; + } + + if (__result.PaintExterior && exterior is not null && SkinProvider.TryGetTheme(exterior, out var exteriorTheme)) + { + __result.PaintExterior.CurrentTheme = exteriorTheme; + } + } + } + } + + [HarmonyPatch(typeof(TrainCar))] + internal static class TrainCarPatches + { + [HarmonyPatch(nameof(TrainCar.InitializeObjectPaint))] + [HarmonyPrefix] + public static void BeforeInitializePaint(GameObject obj) + { + if (obj.GetComponent()) return; + + foreach (var paint in obj.GetComponents()) + { + Object.Destroy(paint); + } + } + + [HarmonyPatch(nameof(TrainCar.InitializeObjectPaint))] + [HarmonyPostfix] + public static void AfterInitializePaint(TrainCar __instance) + { + TrainCarPaintPatches.ReapplyThemes(__instance); + } + } +} diff --git a/SkinManagerMod/Patches/PaintCanPatches.cs b/SkinManagerMod/Patches/PaintCanPatches.cs new file mode 100644 index 0000000..1b50f1b --- /dev/null +++ b/SkinManagerMod/Patches/PaintCanPatches.cs @@ -0,0 +1,38 @@ +using DV.Customization.Paint; +using DV.Interaction; +using DV.ThingTypes; +using HarmonyLib; + +namespace SkinManagerMod.Patches +{ + [HarmonyPatch(typeof(PaintCan))] + internal static class PaintCanPatches + { + [HarmonyPatch(nameof(PaintCan.CheckPaintApplicationValidity))] + [HarmonyPrefix] + public static void CheckPaintValidity(ref bool isCareerMode) + { + if (isCareerMode && Main.Settings.allowPaintingUnowned) + { + isCareerMode = false; + } + } + + [HarmonyPatch(nameof(PaintCan.CheckPaintApplicationValidity))] + [HarmonyPostfix] + public static void FixAlreadyPaintedValidity(ref PaintCan.Validity __result, PaintTheme themeFrom, TrainCar target) + { + if ((__result == PaintCan.Validity.Incompatible) && themeFrom) + { + if (CarTypes.IsRegularCar(target.carLivery)) + { + __result = PaintCan.Validity.Ok; + } + else + { + __result = PaintCan.Validity.AlreadyPainted; + } + } + } + } +} diff --git a/SkinManagerMod/Patches/PaintSprayerPatches.cs b/SkinManagerMod/Patches/PaintSprayerPatches.cs new file mode 100644 index 0000000..4310707 --- /dev/null +++ b/SkinManagerMod/Patches/PaintSprayerPatches.cs @@ -0,0 +1,53 @@ +using DV.Customization.Paint; +using DV.Interaction; +using DV.InventorySystem; +using DV.Items; +using HarmonyLib; + +namespace SkinManagerMod.Patches +{ + [HarmonyPatch(typeof(PaintSprayer))] + internal static class PaintSprayerPatches + { + [HarmonyPatch(nameof(PaintSprayer.Apply))] + [HarmonyPrefix] + private static bool ApplyTheme(PaintSprayer __instance, TrainCarPaint target, PaintCan ___insertedCan) + { + __instance.paintingProgress = 0f; + + var paintCan = ___insertedCan; + var train = TrainCar.Resolve(target.gameObject); + + if (!SkinProvider.IsThemeable(train.carLivery) && (paintCan.theme.name == SkinProvider.DefaultNewThemeName)) + { + target.CurrentTheme = SkinProvider.CustomDefaultTheme; + } + else + { + target.CurrentTheme = ___insertedCan.theme; + } + __instance.UnUse(); + + // empty can + if (paintCan.emptyCanPrefab != null) + { + Reloadable reloadable = UnityEngine.Object.Instantiate(paintCan.emptyCanPrefab); + RespawnOnDrop component = reloadable.GetComponent(); + if (component != null) + { + component.respawnOnDropThroughFloor = false; + component.ignoreDistanceFromSpawnPosition = true; + } + reloadable.GetComponent().BelongsToPlayer = true; + __instance.socket.Inserted = reloadable; + } + else + { + __instance.socket.Inserted = null; + } + Inventory.Instance.DestroyItem(paintCan.gameObject); + + return false; + } + } +} diff --git a/SkinManagerMod/Patches/ReloadableSocketPatches.cs b/SkinManagerMod/Patches/ReloadableSocketPatches.cs new file mode 100644 index 0000000..d8dd24f --- /dev/null +++ b/SkinManagerMod/Patches/ReloadableSocketPatches.cs @@ -0,0 +1,65 @@ +using DV.CabControls; +using DV.Items; +using DV.JObjectExtstensions; +using DV.Utils; +using HarmonyLib; +using Newtonsoft.Json.Linq; +using SkinManagerMod.Items; +using UnityEngine; + +namespace SkinManagerMod.Patches +{ + [HarmonyPatch(typeof(ReloadableSocket))] + internal static class ReloadableSocketPatches + { + [HarmonyPatch(nameof(ReloadableSocket.SaveDataLoaded))] + [HarmonyPrefix] + private static bool SaveDataLoaded(ReloadableSocket __instance, JObject data) + { + data = data.GetJObject(ReloadableSocket.KEY_INSERTED_ITEM); + if (data is null) + { + return false; + } + + string prefabName = data.GetString(ReloadableSocket.KEY_ITEM_PREFAB_NAME) ?? string.Empty; + GameObject can; + + if (PaintFactory.TryParseDummyPrefabName(prefabName, out string? themeName) && + SkinProvider.TryGetTheme(themeName!, out var theme)) + { + // skin manager can + can = PaintFactory.InstantiateCustomCan(theme, __instance.transform.position, Quaternion.identity); + } + else + { + // other item + GameObject prefab = Resources.Load(prefabName); + if (!prefab) + { + Debug.LogError($"[Reloadable] Couldn't find item prefab with the name '{prefabName}'. Loading of item '{prefabName}' skipped."); + return false; + } + can = Object.Instantiate(prefab, __instance.transform.position, Quaternion.identity); + } + + can.name = prefabName; + can.GetComponent().BelongsToPlayer = data.GetBool(ReloadableSocket.KEY_ITEM_BELONGS_TO_PLAYER) ?? false; + + ItemSaveData saveData = can.GetComponent(); + saveData?.LoadItemData(data.GetJObject(ReloadableSocket.KEY_ITEM_SAVE_DATA)); + + if (!can.TryGetComponent(out var reloadableScript)) + { + Debug.LogError($"[Reloadable] Loaded reloadable '{prefabName}' is no longer a reloadable! Moving to Lost and Found."); + StorageController.Instance.AddItemToLostAndFound(can.GetComponent()); + } + else + { + __instance.Inserted = reloadableScript; + } + + return false; + } + } +} diff --git a/SkinManagerMod/SaveLoadPatches.cs b/SkinManagerMod/Patches/SaveLoadPatches.cs similarity index 97% rename from SkinManagerMod/SaveLoadPatches.cs rename to SkinManagerMod/Patches/SaveLoadPatches.cs index 86640f9..ddc8f4b 100644 --- a/SkinManagerMod/SaveLoadPatches.cs +++ b/SkinManagerMod/Patches/SaveLoadPatches.cs @@ -1,7 +1,7 @@ using HarmonyLib; using Newtonsoft.Json.Linq; -namespace SkinManagerMod +namespace SkinManagerMod.Patches { [HarmonyPatch(typeof(SaveGameManager))] internal static class SaveGameManager_Save_Patch diff --git a/SkinManagerMod/Items/ShopPatches.cs b/SkinManagerMod/Patches/ShopPatches.cs similarity index 91% rename from SkinManagerMod/Items/ShopPatches.cs rename to SkinManagerMod/Patches/ShopPatches.cs index 37e25dc..5f47a22 100644 --- a/SkinManagerMod/Items/ShopPatches.cs +++ b/SkinManagerMod/Patches/ShopPatches.cs @@ -1,16 +1,12 @@ -using DV.CabControls; -using DV.CabControls.Spec; -using DV.Shops; +using DV.Shops; using HarmonyLib; -using System; -using System.Collections; +using SkinManagerMod.Items; using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Reflection.Emit; using UnityEngine; -namespace SkinManagerMod.Items +namespace SkinManagerMod.Patches { [HarmonyPatch] static internal class ShopPatches @@ -77,10 +73,10 @@ static IEnumerable InstantiatePurchasedItems(IEnumerable TranspileInstantiateItem(IEnumerable instructions) + { + // replace first chunk of method that checks for a valid prefab + + // push itemData [StorageItemData] + yield return new CodeInstruction(OpCodes.Ldarg_1); + + // push this [StartingItemsController] + yield return new CodeInstruction(OpCodes.Ldarg_0); + + // pop/push this.instantiatedItemCount + var itemCountField = AccessTools.Field(typeof(StartingItemsController), nameof(StartingItemsController.instantiatedItemCount)); + yield return new CodeInstruction(OpCodes.Ldfld, itemCountField); + + // pop/pop/call CustomInstantiateItem(itemData, instantiatedItemCount) => push result [GameObject] + yield return CodeInstruction.Call(typeof(InventoryPatches), nameof(CustomInstantiateItem)); + + // skip everything up until the instantiate call in original method, then continue with the original code + bool skipping = true; + foreach (var instruction in instructions) + { + if (skipping) + { + if (instruction.Calls(nameof(Object.Instantiate))) + { + skipping = false; + } + } + else + { + yield return instruction; + } + + //if (instruction.opcode == OpCodes.Stloc_3) + //{ + // Main.LogVerbose("Patched instantiate item"); + // yield return new CodeInstruction(OpCodes.Ldarg_1); // push itemData + // yield return CodeInstruction.Call(typeof(InventoryPatches), nameof(CustomInstantiateItem)); + // previous = instruction; + //} + //else + //{ + // if (previous != null) yield return previous; + // previous = instruction; + //} + } + + //if (previous != null) yield return previous; + } + + private static GameObject? CustomInstantiateItem(StorageItemData itemData, int instantiatedItemCount) + { + Vector3 position = StartingItemsController.ITEM_INSTANTIATION_SAFETY_POSITION + + (StartingItemsController.ITEM_INSTANTIATION_SAFETY_OFFSET * instantiatedItemCount); + + string prefabName = itemData.itemPrefabName; + + // check for skin manager paint can theme key + if (itemData.state != null && itemData.state.TryGetValue(Constants.CUSTOM_THEME_SAVEDATA_KEY, out JToken? themeToken)) + { + if ((string?)themeToken is string themeName) + { + if (SkinProvider.TryGetTheme(themeName, out var theme)) + { + Main.Log($"Creating saved custom paint can for theme {themeName}"); + return PaintFactory.InstantiateCustomCan(theme, position, Quaternion.identity); + } + else + { + Main.Error($"Couldn't find theme {themeName} for custom paint can"); + prefabName = PaintFactory.DEFAULT_CAN_PREFAB_NAME; + } + } + } + + // not a skin manager item, fallback to default + if (string.IsNullOrEmpty(prefabName)) + { + return null; + } + + if (Resources.Load(prefabName) is not GameObject prefab) + { + Debug.LogError($"Couldn't find item prefab with the name '{prefabName}'. Loading of item '{prefabName}' skipped."); + return null; + } + + return Object.Instantiate(prefab, position, Quaternion.identity); + } + + #endregion + + private static bool Calls(this CodeInstruction instruction, string methodName) + { + if (instruction.operand is MethodInfo method) + { + return method.Name == methodName; + } + return false; + } + } +} diff --git a/SkinManagerMod/Patches/TrainCarPaintPatches.cs b/SkinManagerMod/Patches/TrainCarPaintPatches.cs new file mode 100644 index 0000000..7a39b23 --- /dev/null +++ b/SkinManagerMod/Patches/TrainCarPaintPatches.cs @@ -0,0 +1,95 @@ +using DV.Customization.Paint; +using HarmonyLib; + +namespace SkinManagerMod.Patches +{ + [HarmonyPatch(typeof(TrainCarPaint))] + internal static class TrainCarPaintPatches + { + [HarmonyPatch(nameof(TrainCarPaint.IsSupported))] + [HarmonyPrefix] + private static bool IsSupportedOverride(TrainCarPaint __instance, PaintTheme theme, ref bool __result) + { + var train = TrainCar.Resolve(__instance.gameObject); + + if (theme is not CustomPaintTheme customTheme) + { + // default theme + return (theme.name == SkinProvider.DefaultThemeName) || (theme.name == SkinProvider.DefaultNewThemeName) || (theme.name == SkinProvider.PrimerThemeName); + } + + __result = customTheme.SupportsVehicle(train.carLivery); + + return false; + } + + [HarmonyPatch(nameof(TrainCarPaint.UpdateTheme))] + [HarmonyPrefix] + private static bool UpdateThemeOverride(TrainCarPaint __instance) + { + if (!__instance.currentTheme) return false; + + __instance.hasChangedWhileDisabled = false; + + var train = TrainCar.Resolve(__instance.gameObject); + PaintArea area = (__instance.targetArea == TrainCarPaint.Target.Interior) ? PaintArea.Interior : PaintArea.Exterior; + + ReapplyThemes(train); + + var theme = GetCustomTheme(__instance, train); + SkinManager.SetAppliedCarSkin(train, theme.name, area); + + return false; + } + + public static void ReapplyThemes(TrainCar train) + { + var extTheme = GetCustomTheme(train.PaintExterior, train); + if (!extTheme) return; + + var intTheme = train.PaintInterior ? GetCustomTheme(train.PaintInterior, train) : null; + + extTheme.Apply(train.gameObject, train); + + if (intTheme is not null) + { + if (train.loadedInterior) + { + intTheme.Apply(train.loadedInterior, train); + } + if (train.interiorLOD) + { + intTheme.Apply(train.interiorLOD.gameObject, train); + } + } + + if (train.loadedExternalInteractables) + { + extTheme.Apply(train.loadedExternalInteractables, train); + } + if (train.loadedDummyExternalInteractables) + { + extTheme.Apply(train.loadedDummyExternalInteractables, train); + } + } + + private static CustomPaintTheme GetCustomTheme(TrainCarPaint paint, TrainCar train) + { + if (!paint || !paint.currentTheme) return null!; + + if (paint.currentTheme is not CustomPaintTheme theme) + { + // default theme + string themeName = paint.currentTheme.name; + + if ((themeName == SkinProvider.DefaultNewThemeName) && !SkinProvider.IsThemeable(train.carLivery)) + { + themeName = SkinProvider.DefaultThemeName; + } + theme = SkinProvider.GetTheme(themeName); + } + + return theme; + } + } +} diff --git a/SkinManagerMod/Properties/AssemblyInfo.cs b/SkinManagerMod/Properties/AssemblyInfo.cs deleted file mode 100644 index cfbc5b2..0000000 --- a/SkinManagerMod/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using SMShared; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle(Constants.MOD_ID)] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct(Constants.MOD_ID)] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("bbdceb20-83d5-4fc8-88f9-0e08df1b1dde")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion(Constants.MOD_VERSION)] -[assembly: AssemblyFileVersion(Constants.MOD_VERSION)] diff --git a/SkinManagerMod/ResourcePack.cs b/SkinManagerMod/ResourcePack.cs index 8a5e048..509c210 100644 --- a/SkinManagerMod/ResourcePack.cs +++ b/SkinManagerMod/ResourcePack.cs @@ -27,12 +27,12 @@ public ResourcePack(string name, string folderPath, TrainCarLivery livery) Livery = livery; } - public static ResourcePack LoadFromFile(string filePath) + public static ResourcePack? LoadFromFile(string filePath) { try { string contents = File.ReadAllText(filePath); - var result = JsonConvert.DeserializeObject(contents); + var result = JsonConvert.DeserializeObject(contents)!; result.FolderPath = Path.GetDirectoryName(filePath); result.Livery = Globals.G.Types.Liveries diff --git a/SkinManagerMod/SkinConfig.cs b/SkinManagerMod/SkinConfig.cs index 777d7c5..1c64e0c 100644 --- a/SkinManagerMod/SkinConfig.cs +++ b/SkinManagerMod/SkinConfig.cs @@ -21,15 +21,17 @@ public class SkinConfig : SkinConfigJson public string FolderPath; [JsonIgnore] - public Skin Skin; + public Skin? Skin; [JsonIgnore] - public List Resources = new List(); + public List Resources = new(); [JsonIgnore] public string[] ResourcePaths => Resources.Select(r => r.FolderPath).ToArray(); +#pragma warning disable CS8618 public SkinConfig() { } +#pragma warning restore CS8618 public SkinConfig(string name, string folderPath, TrainCarLivery livery) { @@ -47,12 +49,12 @@ static SkinConfig() _jsonSettings.Converters.Add(new StringEnumConverter()); } - public static SkinConfig LoadFromFile(string filePath) + public static SkinConfig? LoadFromFile(string filePath) { try { string contents = File.ReadAllText(filePath); - var result = JsonConvert.DeserializeObject(contents, _jsonSettings); + var result = JsonConvert.DeserializeObject(contents, _jsonSettings)!; result.FolderPath = Path.GetDirectoryName(filePath); result.Livery = Globals.G.Types.Liveries diff --git a/SkinManagerMod/SkinManager.cs b/SkinManagerMod/SkinManager.cs index d399901..e8a2f10 100644 --- a/SkinManagerMod/SkinManager.cs +++ b/SkinManagerMod/SkinManager.cs @@ -12,7 +12,8 @@ namespace SkinManagerMod { public static class SkinManager { - private static readonly Dictionary carGuidToAppliedSkinMap = new Dictionary(); + private static readonly Dictionary carGuidToAppliedSkinMap = new(); + private static readonly Dictionary interiorSkinMap = new(); public static void Initialize() { @@ -23,38 +24,64 @@ private static void ReapplySkinToUsers(SkinConfig skinConfig) { if (!CarSpawner.Instance) return; + var theme = SkinProvider.GetTheme(skinConfig.Name); + foreach (var car in CarSpawner.Instance.AllCars.Where(tc => tc.carLivery.id == skinConfig.CarId)) { - var toApply = GetCurrentCarSkin(car, false); + (string? exterior, string? interior) = GetCurrentCarSkin(car, false); + + PaintArea matchingArea = PaintArea.None; + if (exterior == skinConfig.Name) matchingArea |= PaintArea.Exterior; + if (interior == skinConfig.Name) matchingArea |= PaintArea.Interior; - if ((toApply != null) && (toApply == skinConfig.Name)) + if (matchingArea != PaintArea.None) { - ApplySkin(car, toApply); + ApplySkin(car, theme, matchingArea); } } } /// Get the currently assigned skin for given car, or a new one if none is assigned - public static string GetCurrentCarSkin(TrainCar car, bool returnNewSkin = true) + public static (string? exterior, string? interior) GetCurrentCarSkin(TrainCar car, bool returnNewSkin = true) { - if (carGuidToAppliedSkinMap.TryGetValue(car.CarGUID, out var skinName)) + if (!carGuidToAppliedSkinMap.TryGetValue(car.CarGUID, out string? exterior) || + string.IsNullOrWhiteSpace(exterior) || + !SkinProvider.TryGetTheme(exterior, out _)) { - if (string.IsNullOrWhiteSpace(skinName)) return null; - - if (SkinProvider.FindSkinByName(car.carLivery, skinName) is Skin result) + if (returnNewSkin) { - return result.Name; + exterior = SkinProvider.GetNewSkin(car.carLivery); } + else + { + exterior = null; + } + } + + if (!interiorSkinMap.TryGetValue(car.CarGUID, out string? interior) || + string.IsNullOrWhiteSpace(interior) || + !SkinProvider.TryGetTheme(interior, out _)) + { + interior = exterior; + } - return returnNewSkin ? SkinProvider.GetNewSkin(car.carLivery) : null; + return (exterior, interior); } /// Save the specified skin to the given car - public static void SetAppliedCarSkin(TrainCar car, string skinName) + public static void SetAppliedCarSkin(TrainCar car, string skinName, PaintArea area) { - Main.LogVerbose($"Setting saved skin for car {car.ID} to \"{skinName}\""); - carGuidToAppliedSkinMap[car.CarGUID] = skinName; + Main.LogVerbose($"Setting saved skin for car {car.ID} {area} to \"{skinName}\""); + + if (area.HasFlag(PaintArea.Exterior)) + { + carGuidToAppliedSkinMap[car.CarGUID] = skinName; + } + if (area.HasFlag(PaintArea.Interior)) + { + interiorSkinMap[car.CarGUID] = skinName; + } // TODO: support for CCL steam locos (this method only checks if == locosteamheavy) if (CarTypes.IsMUSteamLocomotive(car.carType)) @@ -75,29 +102,9 @@ public static void SetAppliedCarSkin(TrainCar car, string skinName) public static void ApplySkin(TrainCar trainCar, string skinName, PaintArea area = PaintArea.All) { - if (!SkinProvider.IsThemeable(trainCar.carLivery.id)) + if (SkinProvider.TryGetTheme(skinName, out CustomPaintTheme newTheme)) { - ApplyNonThemeSkin(trainCar, skinName); - return; - } - - if (PaintTheme.TryLoad(skinName, out PaintTheme newTheme)) - { - if (area.HasFlag(PaintArea.Interior) && trainCar.PaintInterior) - { - if (trainCar.PaintInterior.IsSupported(newTheme)) - { - trainCar.PaintInterior.CurrentTheme = newTheme; - } - } - - if (area.HasFlag(PaintArea.Exterior) && trainCar.PaintExterior) - { - if (trainCar.PaintExterior.IsSupported(newTheme)) - { - trainCar.PaintExterior.CurrentTheme = newTheme; - } - } + ApplySkin(trainCar, newTheme, area); } else { @@ -105,60 +112,21 @@ public static void ApplySkin(TrainCar trainCar, string skinName, PaintArea area } } - public static void ApplyNonThemeSkinToInterior(TrainCar trainCar, string skinName) + public static void ApplySkin(TrainCar trainCar, CustomPaintTheme newTheme, PaintArea area = PaintArea.All) { - var skin = SkinProvider.FindSkinByName(trainCar.carLivery.id, skinName); - if (skin == null) return; - - var defaultSkin = SkinProvider.GetDefaultSkin(trainCar.carLivery.id); - - if (trainCar.loadedInterior) - { - ApplyNonThemeSkinToTransform(trainCar.loadedInterior, skin, defaultSkin); - } - if (trainCar.loadedExternalInteractables) - { - ApplyNonThemeSkinToTransform(trainCar.loadedExternalInteractables, skin, defaultSkin); - } - if (trainCar.loadedDummyExternalInteractables) - { - ApplyNonThemeSkinToTransform(trainCar.loadedDummyExternalInteractables, skin, defaultSkin); - } - } - - private static void ApplyNonThemeSkin(TrainCar trainCar, string skinName) - { - var skin = SkinProvider.FindSkinByName(trainCar.carLivery.id, skinName); - if (skin == null) return; - - var defaultSkin = SkinProvider.GetDefaultSkin(trainCar.carLivery.id); - ApplyNonThemeSkinToTransform(trainCar.gameObject, skin, defaultSkin); - if (trainCar.loadedInterior) - { - ApplyNonThemeSkinToTransform(trainCar.loadedInterior, skin, defaultSkin); - } - if (trainCar.loadedExternalInteractables) + if (newTheme.SupportsVehicle(trainCar.carLivery)) { - ApplyNonThemeSkinToTransform(trainCar.loadedExternalInteractables, skin, defaultSkin); - } - if (trainCar.loadedDummyExternalInteractables) - { - ApplyNonThemeSkinToTransform(trainCar.loadedDummyExternalInteractables, skin, defaultSkin); - } - - SetAppliedCarSkin(trainCar, skinName); - } + if (area.HasFlag(PaintArea.Interior) && trainCar.PaintInterior) + { + trainCar.PaintInterior.CurrentTheme = newTheme; + } - private static void ApplyNonThemeSkinToTransform(GameObject objectRoot, Skin skin, Skin defaultSkin) - { - foreach (var renderer in objectRoot.GetComponentsInChildren(true)) - { - if (!renderer.material) + if (area.HasFlag(PaintArea.Exterior) && trainCar.PaintExterior) { - continue; + trainCar.PaintExterior.CurrentTheme = newTheme; } - TextureUtility.ApplyTextures(renderer, skin, defaultSkin); + //SetAppliedCarSkin(trainCar, newTheme.name, area); } } @@ -212,11 +180,17 @@ public static void LoadCarsSaveData(JObject carsSaveData) } #endregion + + public static PaintArea ToPaintArea(this TrainCarPaint.Target target) + { + return (target == TrainCarPaint.Target.Interior) ? PaintArea.Interior : PaintArea.Exterior; + } } [Flags] public enum PaintArea { + None = 0, Exterior = 1, Interior = 2, All = Exterior | Interior, diff --git a/SkinManagerMod/SkinManagerMod.csproj b/SkinManagerMod/SkinManagerMod.csproj index bde85bf..c089f73 100644 --- a/SkinManagerMod/SkinManagerMod.csproj +++ b/SkinManagerMod/SkinManagerMod.csproj @@ -1,60 +1,30 @@ - - - + - Debug - AnyCPU - {BBDCEB20-83D5-4FC8-88F9-0E08DF1B1DDE} - Library - Properties - SkinManagerMod + netframework4.8 + 9 SkinManagerMod - v4.8 - 512 - true - - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + 4.2.0 + enable - - - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\Assembly-CSharp.dll - - - - - - - False - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\DV.Utils.dll - - - False - - - False - - - False - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\Newtonsoft.Json.dll - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + @@ -64,68 +34,23 @@ - - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\UnityEngine.dll - - - - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\UnityEngine.CoreModule.dll - - - False - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\UnityEngine.ImageConversionModule.dll - - - D:\Games\SteamLibrary\steamapps\common\Derail Valley\DerailValley_Data\Managed\UnityEngine.IMGUIModule.dll - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2.3.0 - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + - - - curl.exe "https://docs.google.com/spreadsheets/d/1TrI4RuUgCijOuCjxM_WsOO9AV0BO4noTIZIzal3HbnY/export?format=csv&gid=1691364666" -L "https://docs.google.com" -o "$(TargetDir)\translations.csv" >NUL - + + + + \ No newline at end of file diff --git a/SkinManagerMod/SkinProvider.cs b/SkinManagerMod/SkinProvider.cs index e24e3b0..cfb14da 100644 --- a/SkinManagerMod/SkinProvider.cs +++ b/SkinManagerMod/SkinProvider.cs @@ -16,92 +16,82 @@ namespace SkinManagerMod { public class SkinProvider { - public static readonly string DefaultThemeName = "DVRT"; - public static readonly string DefaultNewThemeName = "DVRT_New"; - public static readonly string DemoThemeName = "Relic"; - public static readonly string DemoRustyThemeName = "Relic_Rusty"; + public const string DefaultThemeName = "DVRT"; + public const string DefaultNewThemeName = "DVRT_New"; + public const string DemoThemeName = "Relic"; + public const string DemoRustyThemeName = "Relic_Rusty"; + public const string PrimerThemeName = "Null"; - public static readonly string[] BuiltInThemeNames = { DefaultThemeName, DefaultNewThemeName, DemoThemeName, DemoRustyThemeName }; + public static readonly string[] BuiltInThemeNames = { DefaultThemeName, DefaultNewThemeName, DemoThemeName, DemoRustyThemeName, PrimerThemeName }; - private static PaintTheme[] _builtInThemes = null; - public static PaintTheme[] BuiltInThemes + private static readonly Dictionary _builtInThemeDict = new(); + + public static IEnumerable BuiltInThemes => _builtInThemeDict.Values; + public static PaintTheme PrimerTheme => _builtInThemeDict[PrimerThemeName]; + + public static void CacheDefaultThemes() { - get + foreach (string themeName in BuiltInThemeNames) { - if (_builtInThemes == null) - { - _builtInThemes = new PaintTheme[BuiltInThemeNames.Length]; - for (int i = 0; i < BuiltInThemeNames.Length; i++) - { - PaintTheme.TryLoad(BuiltInThemeNames[i], out var theme); - _builtInThemes[i] = theme; - } - } - return _builtInThemes; + PaintTheme.TryLoad(themeName, out var builtin); + _builtInThemeDict.Add(themeName, builtin); } } - public static PaintTheme GetBuiltinTheme(BaseTheme themeType) - { - PaintTheme result; + public static CustomPaintTheme CustomDefaultTheme => _themeDict[DefaultThemeName]; - switch (themeType) + public static BaseTheme GetThemeTypeByName(string themeName) + { + return themeName switch { - case BaseTheme.DVRT: - case BaseTheme.DVRT_NoDetails: - PaintTheme.TryLoad(DefaultThemeName, out result); - break; - - case BaseTheme.Pristine: - case BaseTheme.Pristine_NoDetails: - PaintTheme.TryLoad(DefaultNewThemeName, out result); - break; - - case BaseTheme.Demonstrator: - case BaseTheme.Demonstrator_NoDetails: - PaintTheme.TryLoad(DemoThemeName, out result); - break; - - case BaseTheme.Relic: - case BaseTheme.Relic_NoDetails: - PaintTheme.TryLoad(DemoRustyThemeName, out result); - break; + DefaultNewThemeName => BaseTheme.Pristine, + DemoThemeName => BaseTheme.Demonstrator, + DemoRustyThemeName => BaseTheme.Relic, + _ => BaseTheme.DVRT, + }; + } - default: - throw new NotImplementedException(); - } + public static CustomPaintTheme GetBaseTheme(BaseTheme themeType) + { + string themeName = (themeType & ~BaseTheme.DVRT_NoDetails) switch + { + BaseTheme.Pristine => DefaultNewThemeName, + BaseTheme.Demonstrator => DemoThemeName, + BaseTheme.Relic => DemoRustyThemeName, + _ => DefaultThemeName, + }; - return result; + return _themeDict[themeName]; } public static bool IsBuiltInTheme(string themeName) => BuiltInThemeNames.Contains(themeName); + public static bool IsBuiltInTheme(PaintTheme theme) => BuiltInThemeNames.Contains(theme.name); - public static string LastSteamerSkin { get; set; } - public static string LastDE6Skin { get; set; } + public static string? LastSteamerSkin { get; set; } + public static string? LastDE6Skin { get; set; } /// Emitted when skin(s) are reloaded from disk - public static event Action SkinsLoaded; + public static event Action? SkinsLoaded; - public static event Action SkinDisabled; - public static event Action SkinUpdated; + public static event Action? SkinDisabled; + public static event Action? SkinUpdated; - private static readonly LinkedList skinConfigs = new LinkedList(); + private static readonly LinkedList skinConfigs = new(); /// Livery ID to SkinGroup mapping - private static readonly Dictionary skinGroups = new Dictionary(); + private static readonly Dictionary skinGroups = new(); public static IEnumerable AllSkinGroups => skinGroups.Values; /// Livery ID to default skin mapping - private static readonly Dictionary defaultSkins = new Dictionary(); + private static readonly Dictionary defaultSkins = new(); - private static readonly Dictionary> cachedCarTextures = - new Dictionary>(); + private static readonly Dictionary> cachedCarTextures = new(); // Skin Name to Paint Theme (combined liveries) - private static PaintTheme[] _cachedThemeList = null; - private static readonly Dictionary _themeDict = new Dictionary(); - private static readonly Dictionary _themeSettings = new Dictionary(); + private static PaintTheme[]? _cachedThemeList = null; + private static readonly Dictionary _themeDict = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary _themeSettings = new(); private static readonly HashSet _themeableLiveries = Globals.G.Types.Liveries @@ -112,24 +102,18 @@ public static PaintTheme GetBuiltinTheme(BaseTheme themeType) public static bool IsThemeable(TrainCarLivery livery) => _themeableLiveries.Contains(livery.id); public static bool IsThemeable(string liveryId) => _themeableLiveries.Contains(liveryId); - public static IEnumerable ThemeableLiveryIds => _themeableLiveries; - public static IEnumerable ThemeableSkinGroups => skinGroups.Where(g => _themeableLiveries.Contains(g.Key)).Select(g => g.Value); - - public static PaintTheme[] PaintThemes + public static PaintTheme[] ModdedThemes { get { - if (_cachedThemeList is null) - { - _cachedThemeList = _themeDict.Values.ToArray(); - } + _cachedThemeList ??= _themeDict.Values.Where(t => !BuiltInThemeNames.Contains(t.name)).ToArray(); return _cachedThemeList; } } public static List GetRandomizedStoreThemes() { - var arr = PaintThemes.Where(IsThemeAllowedInStore).ToList(); + var arr = ModdedThemes.Where(IsThemeAllowedInStore).ToList(); // Fisher Yates shuffle for (int i = arr.Count - 1; i > 0; i--) @@ -150,19 +134,20 @@ public static bool IsThemeAllowedInStore(PaintTheme theme) return true; } - public static bool TryGetTheme(string themeName, out PaintTheme theme) => _themeDict.TryGetValue(themeName, out theme); + public static bool TryGetTheme(string themeName, out CustomPaintTheme theme) => _themeDict.TryGetValue(themeName, out theme); + public static CustomPaintTheme GetTheme(string themeName) => _themeDict[themeName]; - private static void RegisterNewTheme(PaintTheme theme) + private static void RegisterNewTheme(CustomPaintTheme theme) { - string lowerName = theme.name.ToLower(); - if (PaintTheme.loadedThemes.ContainsKey(lowerName)) + string key = theme.name.ToLower(); + if (PaintTheme.loadedThemes.ContainsKey(key)) { Main.Error($"Skin \"{theme.name}\" conflicts with an existing or built-in paint theme"); return; } _themeDict.Add(theme.name, theme); - PaintTheme.loadedThemes.Add(lowerName, theme); + PaintTheme.loadedThemes.Add(key, theme); _cachedThemeList = null; } @@ -197,26 +182,28 @@ public static SkinGroup GetSkinGroup(TrainCarLivery livery) return newGroup; } - public static Skin GetDefaultSkin(string carId) - { - if (defaultSkins.TryGetValue(carId, out Skin existing)) - { - return existing; - } + //public static Skin GetDefaultSkin(string carId, BaseTheme baseTheme = BaseTheme.DVRT) + //{ + // string key = $"{carId}_{baseTheme}"; + // if (defaultSkins.TryGetValue(key, out Skin existing)) + // { + // return existing; + // } - var newDefault = CreateDefaultSkin(Globals.G.Types.Liveries.First(l => l.id == carId)); - defaultSkins[carId] = newDefault; - return newDefault; - } + // var livery = Globals.G.Types.Liveries.First(l => l.id == carId); + // var newDefault = CreateDefaultSkin(livery, baseTheme); + // defaultSkins[carId] = newDefault; + // return newDefault; + //} - public static Skin FindSkinByName(TrainCarLivery carType, string name) => FindSkinByName(carType.id, name); + public static Skin? FindSkinByName(TrainCarLivery carType, string name) => FindSkinByName(carType.id, name); - public static Skin FindSkinByName(string carId, string name) + public static Skin? FindSkinByName(string carId, string name) { - if (name == Skin.GetDefaultSkinName(carId)) - { - return GetDefaultSkin(carId); - } + //if (name == Skin.GetDefaultSkinName(carId)) + //{ + // return GetDefaultSkin(carId); + //} if (skinGroups.TryGetValue(carId, out var group)) { @@ -225,10 +212,10 @@ public static Skin FindSkinByName(string carId, string name) return null; } - public static List GetSkinsForType(TrainCarLivery carType, bool includeDefault = true, bool sort = true) => + public static List GetSkinsForType(TrainCarLivery carType, bool includeDefault = true, bool sort = true) => GetSkinsForType(carType.id, includeDefault, sort); - public static List GetSkinsForType(string carId, bool includeDefault = true, bool sort = true) + public static List GetSkinsForType(string carId, bool includeDefault = true, bool sort = true) { var result = new List(); @@ -253,7 +240,7 @@ public static List GetSkinsForType(string carId, bool includeDefault = t } else { - result.Add(Skin.GetDefaultSkinName(carId)); + result.Add(DefaultThemeName); } } @@ -261,7 +248,7 @@ public static List GetSkinsForType(string carId, bool includeDefault = t { result.Sort(); } - return result; + return result.Select(name => _themeDict[name]).ToList(); } private static int CompareSkins(Skin a, Skin b) @@ -286,7 +273,7 @@ public static string GetNewSkin(TrainCarLivery carType) { if (CarTypes.IsTender(carType) && (LastSteamerSkin != null)) { - if (FindSkinByName(carType, LastSteamerSkin) is Skin) + if (FindSkinByName(carType, LastSteamerSkin) is not null) { return LastSteamerSkin; } @@ -294,26 +281,27 @@ public static string GetNewSkin(TrainCarLivery carType) if ((carType.id == Constants.SLUG_LIVERY_ID) && (LastDE6Skin != null)) { - if (FindSkinByName(carType, LastDE6Skin) is Skin || Main.Settings.allowDE6SkinsForSlug) + if (FindSkinByName(carType, LastDE6Skin) is not null || Main.Settings.allowDE6SkinsForSlug) { return LastDE6Skin; } } // random skin - if (skinGroups.TryGetValue(carType.id, out var group) && (group.Skins.Count > 0)) + if (Main.Settings.defaultSkinsMode != DefaultSkinsMode.PreferDefaults) { - bool allowRandomDefault = - (Main.Settings.defaultSkinsMode == DefaultSkinsMode.AllowForAllCars); - // || (CustomCarTypes.ContainsKey(carType) && (Main.Settings.defaultSkinsMode == SkinManagerSettings.DefaultSkinsMode.AllowForCustomCars)); + if (skinGroups.TryGetValue(carType.id, out var group) && (group.Skins.Count > 0)) + { + bool allowRandomDefault = (Main.Settings.defaultSkinsMode == DefaultSkinsMode.AllowForAllCars); - var allowedRandom = group.Skins.Where(AllowRandomSpawning).ToList(); - int nChoices = allowRandomDefault ? allowedRandom.Count + 1 : allowedRandom.Count; + var allowedRandom = group.Skins.Where(AllowRandomSpawning).ToList(); + int nChoices = allowRandomDefault ? allowedRandom.Count + 1 : allowedRandom.Count; - int choice = UnityEngine.Random.Range(0, nChoices); - if (choice < allowedRandom.Count) - { - return allowedRandom[choice].Name; + int choice = UnityEngine.Random.Range(0, nChoices); + if (choice < allowedRandom.Count) + { + return allowedRandom[choice].Name; + } } } @@ -337,6 +325,7 @@ private static bool AllowRandomSpawning(Skin skin) public static bool Initialize() { + InjectNewDefaultThemes(); ReloadAllSkins(); UnityModManager.toggleModsListen += HandleSkinModToggle; @@ -424,12 +413,7 @@ private static void UnloadSkin(string liveryId, string skinName) if (_themeDict.TryGetValue(skinName, out var theme)) { - UnMergeSubstitutions(theme, existingSkin.GetSubstitutions()); - - if (theme.substitutions.Length == 0) - { - UnregisterTheme(skinName); - } + theme.RemoveSkin(liveryId); } } } @@ -563,12 +547,18 @@ private static void TryLoadThemeConfigFile(string configPath) string contents = File.ReadAllText(configPath); var parsedConfig = JsonConvert.DeserializeObject(contents); - if ((parsedConfig.Themes is null) || (parsedConfig.Themes.Length == 0)) + if ((parsedConfig!.Themes is null) || (parsedConfig.Themes.Length == 0)) { Main.Warning($"Found theme config file, but it is empty: {configPath}"); return; } + if (string.IsNullOrEmpty(parsedConfig.Version) || !Version.TryParse(parsedConfig.Version, out var version)) + { + Main.Warning($"Theme config contains a missing or invalid version: {configPath}"); + return; + } + foreach (var configItem in parsedConfig.Themes) { if (string.IsNullOrEmpty(configItem.Name)) @@ -577,21 +567,15 @@ private static void TryLoadThemeConfigFile(string configPath) continue; } - if (string.IsNullOrEmpty(parsedConfig.Version) || !Version.TryParse(parsedConfig.Version, out var version)) - { - Main.Warning($"Theme config contains a missing or invalid version: {configPath}"); - continue; - } - var newSettings = ThemeSettings.Create(configPath, configItem, version); - if (TryGetThemeSettings(configItem.Name, out var settings)) + if (TryGetThemeSettings(configItem.Name!, out var settings)) { settings.Merge(newSettings); } else { - _themeSettings.Add(configItem.Name, newSettings); + _themeSettings.Add(configItem.Name!, newSettings); } } } @@ -615,22 +599,67 @@ private static Dictionary GetCarTextureDictionary(TrainCarLivery /// /// Create a skin containing the default/starting textures of a car /// - private static Skin CreateDefaultSkin(TrainCarLivery carType) + private static void InjectNewDefaultThemes() { - GameObject carPrefab = carType.prefab; - if (carPrefab == null) return null; - - var defaultSkin = Skin.Default(carType.id); + var primerTexture = (Texture2D)PrimerTheme.substitutions.First().substitute.mainTexture; - foreach (var texture in TextureUtility.EnumerateTextures(carType)) + foreach (string themeName in BuiltInThemeNames) { - if (!defaultSkin.ContainsTexture(texture.name)) + BaseTheme themeType = GetThemeTypeByName(themeName); + var existingTheme = _builtInThemeDict[themeName]; + + var customDefaultTheme = ScriptableObject.CreateInstance(); + UnityEngine.Object.DontDestroyOnLoad(customDefaultTheme); + customDefaultTheme.assetName = existingTheme.assetName; + customDefaultTheme.name = existingTheme.name; + customDefaultTheme.nameLocalizationKey = existingTheme.nameLocalizationKey; + customDefaultTheme.substitutions = existingTheme.substitutions; + + foreach (var carType in Globals.G.Types.Liveries) { - defaultSkin.SkinTextures.Add(new SkinTexture(texture.name, texture)); + if ((themeName == DefaultThemeName) || IsThemeable(carType)) + { + var defaultSkin = Skin.Default(carType.id, themeType); + customDefaultTheme.AddSkin(defaultSkin); + } + else if ((themeName == PrimerThemeName) && !IsThemeable(carType)) + { + // extend primer coverage to all cars + var defaultSkin = Skin.Default(carType.id, themeType); + string bodyName = DefaultTextures.GetBodyTextureName(carType.id); + defaultSkin.SkinTextures.Add(new SkinTexture(bodyName, primerTexture)); + customDefaultTheme.AddSkin(defaultSkin); + } } + + _themeDict[themeName] = customDefaultTheme; + //PaintTheme.loadedThemes[themeName.ToLower()] = customDefaultTheme; } - return defaultSkin; + // primer + PaintTheme.TryLoad(PrimerThemeName, out PaintTheme defaultPrimer); + + var customPrimer = ScriptableObject.CreateInstance(); + UnityEngine.Object.DontDestroyOnLoad(customPrimer); + customPrimer.assetName = defaultPrimer.assetName; + customPrimer.name = defaultPrimer.name; + customPrimer.nameLocalizationKey = defaultPrimer.nameLocalizationKey; + customPrimer.substitutions = defaultPrimer.substitutions; + + foreach (var carType in Globals.G.Types.Liveries) + { + if (IsThemeable(carType)) + { + var fakeSkin = Skin.Default(carType.id, BaseTheme.DVRT); + var carData = CarMaterialData.GetDataForCar(carType.id); + + foreach (var substitution in customPrimer.substitutions + .Where(sub => carData.GetDataForMaterial(sub.original) is not null)) + { + + } + } + } } /// @@ -648,11 +677,11 @@ private static bool TryGetTextureForFilename(string liveryId, ref string filenam return true; } - if (Remaps.TryGetUpdatedTextureName(liveryId, filename, out string newName)) + if (Remaps.TryGetUpdatedTextureName(liveryId, filename, out string? newName)) { - if (textureNames.TryGetValue(newName, out textureProp)) + if (textureNames.TryGetValue(newName!, out textureProp)) { - filename = newName; + filename = newName!; return true; } } @@ -682,7 +711,7 @@ internal static void BeginLoadResources(ResourcePack config, bool forceSync = fa string fileName = Path.GetFileNameWithoutExtension(texturePath); - if (TryGetTextureForFilename(config.CarId, ref fileName, textureNames, out string textureProp)) + if (TryGetTextureForFilename(config.CarId!, ref fileName, textureNames, out string textureProp)) { var linear = textureProp == "_BumpMap"; @@ -749,10 +778,10 @@ internal static void BeginLoadSkin(SkinConfig config, bool forceSync = false) skinGroup.Skins.Add(skin); - if (skin.IsThemeable && !_themeDict.TryGetValue(skin.Name, out var theme)) + if (!_themeDict.TryGetValue(skin.Name, out var theme)) { Main.LogVerbose($"Create new theme {skin.Name}"); - theme = ScriptableObject.CreateInstance(); + theme = ScriptableObject.CreateInstance(); UnityEngine.Object.DontDestroyOnLoad(theme); theme.assetName = skin.Name; theme.name = skin.Name; @@ -762,9 +791,15 @@ internal static void BeginLoadSkin(SkinConfig config, bool forceSync = false) RegisterNewTheme(theme); } + else + { + Main.LogVerbose($"Merging into theme {skin.Name}"); + } + + theme.AddSkin(skin); - skin.LoadingFinished += AddSkinTexturesToTheme; - skin.StartLoadFinishedListener(); + //skin.LoadingFinished += AddSkinTexturesToTheme; + //skin.StartLoadFinishedListener(); } private static SkinTexture BeginLoadTexture(string fileName, ResourceConfigJson config, string texturePath, bool linear, bool forceSync) @@ -785,14 +820,13 @@ private static SkinTexture BeginLoadTexture(string fileName, ResourceConfigJson private static void AddSkinTexturesToTheme(Skin skin) { - if (!skin.IsThemeable) return; - - var subs = skin.GetSubstitutions(); + //var subs = skin.GetSubstitutions(); if (_themeDict.TryGetValue(skin.Name, out var theme)) { Main.LogVerbose($"Merging into theme {skin.Name}"); - MergeSubstitutions(theme, subs); + //MergeSubstitutions(theme, subs); + theme.AddSkin(skin); } else { @@ -800,50 +834,6 @@ private static void AddSkinTexturesToTheme(Skin skin) } } - private static void MergeSubstitutions(PaintTheme theme, PaintTheme.Substitution[] toMerge) - { - if (toMerge is null || toMerge.Length == 0) return; - - toMerge = toMerge.Where(sub => !theme.substitutions.Any(existSub => SubstitutesSameMaterial(sub, existSub))).ToArray(); - - int currentLength = theme.substitutions.Length; - var newArray = new PaintTheme.Substitution[currentLength + toMerge.Length]; - - Array.Copy(theme.substitutions, newArray, currentLength); - Array.Copy(toMerge, 0, newArray, currentLength, toMerge.Length); - - theme.substitutions = newArray; - theme.substitutionDictionary = null; - } - - public static void UnMergeSubstitutions(PaintTheme theme, PaintTheme.Substitution[] toRemove) - { - if (toRemove is null || toRemove.Length == 0) return; - - var result = new List(theme.substitutions.Length - toRemove.Length); - - foreach (var substitution in theme.substitutions) - { - if (!toRemove.Any(s => SubstitutionsMatch(s, substitution))) - { - result.Add(substitution); - } - } - - theme.substitutions = result.ToArray(); - theme.substitutionDictionary = null; - } - - private static bool SubstitutesSameMaterial(PaintTheme.Substitution a, PaintTheme.Substitution b) - { - return a.original == b.original; - } - - private static bool SubstitutionsMatch(PaintTheme.Substitution a, PaintTheme.Substitution b) - { - return (a.original == b.original) && (a.substitute == b.substitute); - } - #endregion @@ -897,7 +887,7 @@ private static IEnumerable LoadAllSkinsForType(string parentFolder, } } - if (Remaps.TryGetOldTrainCarId(livery.id, out string overhauledId)) + if (Remaps.TryGetOldTrainCarId(livery.id, out string? overhauledId)) { folderPath = Path.Combine(parentFolder, overhauledId); diff --git a/SkinManagerMod/Skins.cs b/SkinManagerMod/Skins.cs index 6547291..ddeff87 100644 --- a/SkinManagerMod/Skins.cs +++ b/SkinManagerMod/Skins.cs @@ -14,41 +14,13 @@ public class Skin { public readonly string LiveryId; public readonly string Name; - public readonly string Path; + public readonly string? Path; public readonly bool IsDefault; - public readonly List SkinTextures = new List(); - public readonly string[] ResourcePaths; + public readonly List SkinTextures = new(); + public readonly string[]? ResourcePaths; public readonly BaseTheme BaseTheme; - public readonly bool IsThemeable; - - public event Action LoadingFinished; - - private PaintTheme.Substitution[] _cachedSubstitutions = null; - - public void StartLoadFinishedListener() - { - var toAwait = SkinTextures - .Select(t => t.LoadingTask) - .Where(t => !(t is null) && !t.IsCompleted); - - var taskArr = toAwait.ToArray(); - if ((taskArr.Length == 0) || taskArr.All(t => t.IsCompleted)) - { - LoadingFinished?.Invoke(this); - } - else - { - Task.WhenAll(taskArr).ContinueWith(OnLoadFinished); - } - } - - private void OnLoadFinished(Task _ = null) - { - ThreadHelper.Instance.EnqueueAction(() => LoadingFinished?.Invoke(this)); - } - - private Skin(string liveryId, string name, string directory, bool isDefault, string[] resourcePaths, BaseTheme baseTheme) + private Skin(string liveryId, string name, string? directory, bool isDefault, string[]? resourcePaths, BaseTheme baseTheme) { LiveryId = liveryId; Name = name; @@ -56,11 +28,9 @@ private Skin(string liveryId, string name, string directory, bool isDefault, str IsDefault = isDefault; ResourcePaths = resourcePaths; BaseTheme = baseTheme; - - IsThemeable = SkinProvider.IsThemeable(liveryId); } - public static Skin Custom(string liveryId, string name, string directory, BaseTheme baseTheme, string[] resourcePaths = null) + public static Skin Custom(string liveryId, string name, string directory, BaseTheme baseTheme, string[]? resourcePaths = null) { return new Skin(liveryId, name, directory, false, resourcePaths, baseTheme); } @@ -70,9 +40,9 @@ public static Skin Custom(SkinConfig config) return new Skin(config.CarId, config.Name, config.FolderPath, false, config.ResourcePaths, config.BaseTheme); } - public static Skin Default(string liveryId) + public static Skin Default(string liveryId, BaseTheme baseTheme) { - return new Skin(liveryId, GetDefaultSkinName(liveryId), null, true, null, BaseTheme.DVRT); + return new Skin(liveryId, GetDefaultSkinName(liveryId), null, true, null, baseTheme); } public bool ContainsTexture(string name) @@ -80,12 +50,12 @@ public bool ContainsTexture(string name) return GetTexture(name) != null; } - public SkinTexture GetTexture(string name) + public SkinTexture? GetTexture(string name) { return SkinTextures.Find(tex => tex.Name == name); } - public FileInfo GetResource(string filename) + public FileInfo? GetResource(string filename) { string absPath = System.IO.Path.Combine(Path, filename); if (File.Exists(absPath)) @@ -93,6 +63,8 @@ public FileInfo GetResource(string filename) return new FileInfo(absPath); } + if (ResourcePaths is null) return null; + foreach (string resourceFolder in ResourcePaths) { absPath = System.IO.Path.Combine(resourceFolder, filename); @@ -105,67 +77,6 @@ public FileInfo GetResource(string filename) return null; } - public PaintTheme.Substitution[] GetSubstitutions() - { - if (!(_cachedSubstitutions is null)) return _cachedSubstitutions; - - var carData = CarMaterialData.GetDataForCar(LiveryId); - - // map default material to new material - var subMap = new Dictionary(); - - foreach (var texture in SkinTextures) - { - var exteriorUses = carData.Exterior.GetTextureAssignments(texture.Name); - MapTextureUsesToNewMaterial(texture, subMap, exteriorUses); - - var interiorUses = carData.Interior.GetTextureAssignments(texture.Name); - MapTextureUsesToNewMaterial(texture, subMap, interiorUses); - } - - var subs = subMap - .Select(kvp => new PaintTheme.Substitution { original = kvp.Key, substitute = kvp.Value }) - .ToArray(); - - _cachedSubstitutions = subs; - return subs; - } - - private static Material GetBaseMaterial(Material defaultMaterial, BaseTheme themeType) - { - var result = SkinProvider.GetBuiltinTheme(themeType); - - if (result && result.TryGetSubstitute(defaultMaterial, out var substitution) && substitution.substitute) - { - return substitution.substitute; - } - return defaultMaterial; - } - - private void MapTextureUsesToNewMaterial(SkinTexture texture, Dictionary substitutions, IEnumerable uses) - { - foreach (var use in uses) - { - if (!substitutions.TryGetValue(use.Material, out Material newMaterial)) - { - var baseMaterial = GetBaseMaterial(use.Material, BaseTheme); - newMaterial = new Material(baseMaterial); - - if (BaseTheme.HasFlag(BaseTheme.DVRT_NoDetails)) - { - foreach (string propName in TextureUtility.PropNames.DetailTextures) - { - newMaterial.SetTexture(propName, null); - } - } - - substitutions.Add(use.Material, newMaterial); - } - - texture.RunOnLoadingComplete(t => newMaterial.SetTexture(use.PropertyName, t.TextureData)); - } - } - public static string GetDefaultSkinName(string liveryId) => $"Default_{liveryId}"; } @@ -174,18 +85,18 @@ public class SkinTexture public readonly string Name; public readonly DateTime LastModified; - private Task task; - public Task LoadingTask => task; + private Task? task; + public Task? LoadingTask => task; - private Texture2D _textureData; + private Texture2D? _textureData; public Texture2D TextureData { get { - if (_textureData == null) + if (_textureData is null) { - _textureData = task.Result; + _textureData = task!.Result!; task = null; // need to set name for reskinning to work @@ -222,7 +133,7 @@ public SkinTexture(string name, Texture2D textureData, DateTime? lastModified = LastModified = lastModified ?? DateTime.MinValue; } - public SkinTexture(string name, Task task, DateTime lastModified) + public SkinTexture(string name, Task task, DateTime lastModified) { Name = name; this.task = task; diff --git a/SkinManagerMod/TextureLoader.cs b/SkinManagerMod/TextureLoader.cs index 67c254d..30c2304 100644 --- a/SkinManagerMod/TextureLoader.cs +++ b/SkinManagerMod/TextureLoader.cs @@ -1,8 +1,7 @@ +using Microsoft.SqlServer.Server; using SMShared.Json; using System; using System.IO; -using System.IO.Compression; -using System.Text; using System.Threading.Tasks; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; @@ -25,21 +24,21 @@ public static void BustCache(ResourceConfigJson skin, string texturePath) } } - private static Task TryLoadFromCache(ResourceConfigJson skin, string texturePath, bool isNormalMap) + private static Task TryLoadFromCache(ResourceConfigJson skin, string texturePath, bool isNormalMap) { var texFile = new FileInfo(texturePath); var cached = new FileInfo(GetCachePath(skin, texturePath)); if (!cached.Exists) { - return Task.FromResult(null); + return Task.FromResult(null); } if (cached.LastWriteTimeUtc < texFile.LastWriteTimeUtc) { Main.LogVerbose($"Cached texture {cached.FullName} is out of date"); BustCache(skin, texturePath); - return Task.FromResult(null); + return Task.FromResult(null); } try @@ -50,11 +49,11 @@ private static Task TryLoadFromCache(ResourceConfigJson skin, string { Main.Warning($"Error loading cached texture {cached.FullName}: {e.Message}"); BustCache(skin, texturePath); - return Task.FromResult(null); + return Task.FromResult(null); } } - public static Task LoadAsync(ResourceConfigJson skin, string texturePath, bool isNormalMap) + public static Task LoadAsync(ResourceConfigJson skin, string texturePath, bool isNormalMap) { var cached = TryLoadFromCache(skin, texturePath, isNormalMap); if (!cached.IsCompleted || cached.Result != null) @@ -69,19 +68,12 @@ public static Task LoadAsync(ResourceConfigJson skin, string textureP var texture = new Texture2D(info.width, info.height, format, mipChain: true, linear: isNormalMap); - var nativeArray = texture.GetRawTextureData(); - Main.LogVerbose($"Loading texture {texturePath} as {format} with StbImage"); - return Task.Run(() => - { - PopulateTexture(texturePath, format, nativeArray); - string cachePath = GetCachePath(skin, texturePath); - Directory.CreateDirectory(Path.GetDirectoryName(cachePath)); - DDSUtils.WriteDDSGz(new FileInfo(cachePath), texture); - return texture; - }); + + var loader = new UncachedReader(skin, texturePath, format, texture); + return loader.Dispatch(); } - public static Texture2D LoadSync(ResourceConfigJson skin, string texturePath, bool isNormalMap) + public static Texture2D LoadSync(ResourceConfigJson _, string texturePath, bool isNormalMap) { var textureFormat = TextureFormat.RGBA32; var texture = new Texture2D(0, 0, textureFormat, mipChain: true, linear: isNormalMap); @@ -100,22 +92,13 @@ private static string GetCachePath(ResourceConfigJson skin, string texturePath) private static void PopulateTexture(string path, TextureFormat textureFormat, NativeArray dest) { - StbImage.TextureFormat format; - switch (textureFormat) + var format = textureFormat switch { - case TextureFormat.DXT1: - format = StbImage.TextureFormat.BC1; - break; - case TextureFormat.DXT5: - format = StbImage.TextureFormat.BC3; - break; - case TextureFormat.BC5: - format = StbImage.TextureFormat.BC5; - break; - default: - throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"); - } - + TextureFormat.DXT1 => StbImage.TextureFormat.BC1, + TextureFormat.DXT5 => StbImage.TextureFormat.BC3, + TextureFormat.BC5 => StbImage.TextureFormat.BC5, + _ => throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"), + }; unsafe { StbImage.ReadAndCompressImageWithMipmaps( @@ -126,224 +109,37 @@ private static void PopulateTexture(string path, TextureFormat textureFormat, Na dest.Length); } } - } - - internal class DDSReadException : Exception - { - public DDSReadException(string message) : base(message) { } - } - - internal static class DDSUtils - { - private static int Mipmap0SizeInBytes(int width, int height, TextureFormat textureFormat) - { - var blockWidth = (width + 3) / 4; - var blockHeight = (height + 3) / 4; - int bytesPerBlock; - switch (textureFormat) - { - case TextureFormat.DXT1: - bytesPerBlock = 8; - break; - case TextureFormat.DXT5: - case TextureFormat.BC5: - bytesPerBlock = 16; - break; - default: - throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"); - } - - return blockWidth * blockHeight * bytesPerBlock; - } - - private const int DDS_HEADER_SIZE = 128; - private const int DDS_HEADER_DXT10_SIZE = 20; - private static byte[] DDSHeader(int width, int height, TextureFormat textureFormat, int numMipmaps) - { - var needsDXGIHeader = textureFormat != TextureFormat.DXT1 && textureFormat != TextureFormat.DXT5; - var headerSize = needsDXGIHeader ? DDS_HEADER_SIZE + DDS_HEADER_DXT10_SIZE : DDS_HEADER_SIZE; - var header = new byte[headerSize]; - using (var stream = new MemoryStream(header)) - { - stream.Write(Encoding.ASCII.GetBytes("DDS "), 0, 4); - stream.Write(BitConverter.GetBytes(124), 0, 4); // dwSize - // dwFlags = CAPS | HEIGHT | WIDTH | PIXELFORMAT | MIPMAPCOUNT | LINEARSIZE - stream.Write(BitConverter.GetBytes(0x1 | 0x2 | 0x4 | 0x1000 | 0x20000 | 0x80000), 0, 4); - stream.Write(BitConverter.GetBytes(height), 0, 4); - stream.Write(BitConverter.GetBytes(width), 0, 4); - stream.Write(BitConverter.GetBytes(Mipmap0SizeInBytes(width, height, textureFormat)), 0, 4); // dwPitchOrLinearSize - stream.Write(BitConverter.GetBytes(0), 0, 4); // dwDepth - stream.Write(BitConverter.GetBytes(numMipmaps), 0, 4); // dwMipMapCount - for (int i = 0; i < 11; i++) - stream.Write(BitConverter.GetBytes(0), 0, 4); // dwReserved1 - var pixelFormat = PixelFormat(textureFormat); - stream.Write(pixelFormat, 0, pixelFormat.Length); - // dwCaps = COMPLEX | MIPMAP | TEXTURE - stream.Write(BitConverter.GetBytes(0x401008), 0, 4); - // dwCaps2, dwCaps3, dwCaps4, dwReserved2 - for (int i = 0; i < 4; i++) - stream.Write(BitConverter.GetBytes(0), 0, 4); - - if (needsDXGIHeader) - stream.Write(DDSHeaderDXT10(textureFormat), 0, DDS_HEADER_DXT10_SIZE); - } - return header; - } - - private static byte[] PixelFormat(TextureFormat textureFormat) - { - string fourCC; - switch (textureFormat) - { - case TextureFormat.DXT1: - fourCC = "DXT1"; - break; - case TextureFormat.DXT5: - fourCC = "DXT5"; - break; - default: - fourCC = "DX10"; - break; - } - - var pixelFormat = new byte[32]; - using (var stream = new MemoryStream(pixelFormat)) - { - stream.Write(BitConverter.GetBytes(32), 0, 4); // dwSize - stream.Write(BitConverter.GetBytes(0x4), 0, 4); // dwFlags = FOURCC - stream.Write(Encoding.ASCII.GetBytes(fourCC), 0, 4); // dwFourCC - } - return pixelFormat; - } - - private static int DXGIFormat(TextureFormat textureFormat) - { - switch (textureFormat) - { - case TextureFormat.BC5: return 83; - default: - throw new ArgumentException("textureFormat", $"Unsupported TextureFormat {textureFormat}"); - } - } - private static byte[] DDSHeaderDXT10(TextureFormat textureFormat) + internal class UncachedReader { - var headerDXT10 = new byte[DDS_HEADER_DXT10_SIZE]; - using (var stream = new MemoryStream(headerDXT10)) - { - stream.Write(BitConverter.GetBytes(DXGIFormat(textureFormat)), 0, 4); // dxgiFormat - stream.Write(BitConverter.GetBytes(3), 0, 4); // resourceDimension = 3 = DDS_DIMENSION_TEXTURE2D - stream.Write(BitConverter.GetBytes(0), 0, 4); // miscFlag - stream.Write(BitConverter.GetBytes(1), 0, 4); // arraySize = 1 - stream.Write(BitConverter.GetBytes(0), 0, 4); // miscFlags2 = 0 = DDS_ALPHA_MODE_UNKNOWN - } - return headerDXT10; - } + private ResourceConfigJson _package; + private string _texturePath; + private TextureFormat _format; + private Texture2D _texture; - public static void WriteDDSGz(FileInfo fileInfo, Texture2D texture) - { - Main.Log($"Writing to {fileInfo.FullName}"); - using (var fileStream = fileInfo.OpenWrite()) - using (var outfile = new GZipStream(fileStream, CompressionLevel.Optimal)) + public UncachedReader(ResourceConfigJson package, string texturePath, TextureFormat format, Texture2D texture) { - var header = DDSHeader(texture.width, texture.height, texture.format, texture.mipmapCount); - outfile.Write(header, 0, header.Length); - - var data = texture.GetRawTextureData().ToArray(); - outfile.Write(data, 0, data.Length); + _package = package; + _texturePath = texturePath; + _format = format; + _texture = texture; } - } - - private static Texture2D ReadDDSHeader(Stream infile, bool linear) - { - var buf = new byte[4096]; - var bytesRead = infile.Read(buf, 0, DDS_HEADER_SIZE); - if (bytesRead != 128 || Encoding.ASCII.GetString(buf, 0, 4) != "DDS ") - throw new DDSReadException("File is not a DDS file"); - - int height = BitConverter.ToInt32(buf, 12); - int width = BitConverter.ToInt32(buf, 16); - int pixelFormatFlags = BitConverter.ToInt32(buf, 80); - if ((pixelFormatFlags & 0x4) == 0) - throw new DDSReadException("DDS header does not have a FourCC"); - string fourCC = Encoding.ASCII.GetString(buf, 84, 4); - TextureFormat textureFormat; - switch (fourCC) + public Task Dispatch() { - case "DXT1": - textureFormat = TextureFormat.DXT1; - break; - case "DXT5": - textureFormat = TextureFormat.DXT5; - break; - case "DX10": - // read DDS_HEADER_DXT10 header extension - bytesRead = infile.Read(buf, 0, DDS_HEADER_DXT10_SIZE); - if (bytesRead != DDS_HEADER_DXT10_SIZE) - throw new DDSReadException("Could not read DXT10 header from DDS file"); - int dxgiFormat = BitConverter.ToInt32(buf, 0); - switch (dxgiFormat) - { - case 83: - textureFormat = TextureFormat.BC5; - break; - default: - throw new DDSReadException($"Unsupported DXGI_FORMAT {dxgiFormat}"); - } - break; - default: - throw new DDSReadException($"Unknown FourCC: {fourCC}"); + return Task.Run(DoLoadAsync); } - var texture = new Texture2D(width, height, textureFormat, true, linear); - return texture; - } - - public static Task ReadDDSGz(FileInfo fileInfo, bool isNormalMap) - { - FileStream fileStream = null; - GZipStream infile = null; - try + private Texture2D? DoLoadAsync() { - fileStream = fileInfo.OpenRead(); - infile = new GZipStream(fileStream, CompressionMode.Decompress); + Main.LogVerbose($"Loading texture {_texturePath} as {_format} with StbImage"); - var texture = ReadDDSHeader(infile, isNormalMap); - if (isNormalMap && texture.format != TextureFormat.BC5) - { - Main.LogVerbose($"Cached normal map texture {fileInfo.FullName} has old format {texture.format}"); - infile.Close(); - fileStream.Close(); - File.Delete(fileInfo.FullName); - return Task.FromResult(null); - } - - Main.LogVerbose($"Reading cached {texture.format} texture from {fileInfo.FullName}"); - var nativeArray = texture.GetRawTextureData(); - return Task.Run(() => - { - try - { - var buf = new byte[nativeArray.Length]; - var bytesRead = infile.Read(buf, 0, nativeArray.Length); - if (bytesRead < nativeArray.Length) - throw new DDSReadException($"{fileInfo.FullName}: Expected {nativeArray.Length} bytes, but file contained {bytesRead}"); - nativeArray.CopyFrom(buf); - return texture; - } - finally - { - infile.Close(); - fileStream.Close(); - } - }); - } - catch (Exception ex) - { - infile?.Close(); - fileStream?.Close(); - throw ex; + var nativeArray = _texture.GetRawTextureData(); + PopulateTexture(_texturePath, _format, nativeArray); + string cachePath = GetCachePath(_package, _texturePath); + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)); + DDSUtils.WriteDDSGz(new FileInfo(cachePath), _texture); + return _texture; } } } diff --git a/SkinManagerMod/TextureUtility.cs b/SkinManagerMod/TextureUtility.cs index 100ff85..27b9539 100644 --- a/SkinManagerMod/TextureUtility.cs +++ b/SkinManagerMod/TextureUtility.cs @@ -23,12 +23,12 @@ public static class PropNames public static readonly string[] UniqueTextures = { - Main, BumpMap, MetalGlossMap, EmissionMap, + Main, BumpMap, MetalGlossMap, EmissionMap, DetailAlbedo, DetailNormal }; public static readonly string[] AllTextures = { - Main, BumpMap, MetalGlossMap, EmissionMap, OcclusionMap, + Main, BumpMap, MetalGlossMap, EmissionMap, OcclusionMap, DetailAlbedo, DetailNormal }; public static readonly string[] DetailTextures = @@ -67,16 +67,17 @@ public static void DumpTextures(TrainCarLivery trainCarType) Main.LogVerbose($"{renderer.name}: {cleanName}"); } - foreach (var theme in SkinProvider.BuiltInThemes) + foreach (var themeName in SkinProvider.BuiltInThemeNames) { - if (theme.name == SkinProvider.DefaultThemeName) continue; + if (themeName == SkinProvider.DefaultThemeName) continue; - string subPath = Path.Combine(path, theme.name); + string subPath = Path.Combine(path, themeName); if (!Directory.Exists(subPath)) { Directory.CreateDirectory(subPath); } + var theme = SkinProvider.GetTheme(themeName); foreach (var substitution in theme.substitutions.Where(s => s.substitute && materialNames.Contains(s.original.name))) { ExportTexturesForMaterial(subPath, substitution.substitute, alreadyExported); @@ -103,7 +104,7 @@ private static void ExportTexturesForMaterial(string path, Material material, Di ExportTexture(path, detailBump, alreadyExported, true); } - private static void ExportTexture(string path, Texture2D texture, Dictionary alreadyExported, bool isNormal = false) + private static void ExportTexture(string path, Texture2D? texture, Dictionary alreadyExported, bool isNormal = false) { if (texture != null && !alreadyExported.ContainsKey(texture.name)) { @@ -176,7 +177,7 @@ public static Texture2D DTXnm2RGBA(Texture2D tex) Color c = colors[i]; c.r = c.a * 2 - 1; //red<-alpha (x<-w) c.g = c.g * 2 - 1; //green is always the same (y) - Vector2 xy = new Vector2(c.r, c.g); //this is the xy vector + var xy = new Vector2(c.r, c.g); //this is the xy vector c.b = Mathf.Sqrt(1 - Mathf.Clamp01(Vector2.Dot(xy, xy))); //recalculate the blue channel (z) colors[i] = new Color(c.r * 0.5f + 0.5f, c.g * 0.5f + 0.5f, c.b * 0.5f + 0.5f); //back to 0-1 range } @@ -189,7 +190,7 @@ public static Texture2D DTXnm2RGBA(Texture2D tex) /// /// Get the named 2D texture property from a material /// - public static Texture2D GetMaterialTexture(Material material, string propName) + public static Texture2D? GetMaterialTexture(Material material, string propName) { if (!material || !material.HasProperty(propName)) { @@ -215,6 +216,20 @@ public static IEnumerable EnumerateTextures(IEnumerable } } + public static IEnumerable EnumerateTextures(IEnumerable materials) + { + foreach (var material in materials) + { + foreach (string textureName in PropNames.UniqueTextures) + { + if (GetMaterialTexture(material, textureName) is Texture2D texture) + { + yield return texture; + } + } + } + } + public static IEnumerable EnumerateTextures(TrainCarLivery livery) { var renderers = GetAllCarRenderers(livery); @@ -278,38 +293,44 @@ public static void SetTextureOptions(Texture2D tex) /// /// Actually assign applicable skin textures to a renderer, using default skin to supply fallbacks /// - public static void ApplyTextures(MeshRenderer renderer, Skin skin, Skin defaultSkin) + public static void ApplyTextures(MeshRenderer renderer, Skin skin, CarMaterialData defaultSkin) { - foreach (string textureID in PropNames.AllTextures) - { - var currentTexture = GetMaterialTexture(renderer.sharedMaterial, textureID); + var defaultData = defaultSkin.GetDataForMaterial(renderer.material); + if (defaultData is null) return; + + var defaultMaterial = defaultData.GetMaterialForBaseTheme(skin.BaseTheme); - if (currentTexture != null) + foreach (var defaultTexture in defaultData.AllTextures) + { + if (skin.ContainsTexture(defaultTexture.TextureName)) { - if (skin.ContainsTexture(currentTexture.name)) - { - var skinTexture = skin.GetTexture(currentTexture.name); - renderer.material.SetTexture(textureID, skinTexture.TextureData); + var skinTexture = skin.GetTexture(defaultTexture.TextureName)!; + renderer.material.SetTexture(defaultTexture.PropertyName, skinTexture.TextureData); - if (textureID == PropNames.MetalGlossMap) + if (defaultTexture.PropertyName == PropNames.MetalGlossMap) + { + if (!GetMaterialTexture(renderer.material, PropNames.OcclusionMap)) { - if (!GetMaterialTexture(renderer.sharedMaterial, PropNames.OcclusionMap)) - { - renderer.material.SetTexture(PropNames.OcclusionMap, skinTexture.TextureData); - } + renderer.material.SetTexture(PropNames.OcclusionMap, skinTexture.TextureData); } } - else if ((defaultSkin != null) && defaultSkin.ContainsTexture(currentTexture.name)) + } + else + { + var skinTexture = defaultMaterial.GetTexture(defaultTexture.PropertyName); + renderer.material.SetTexture(defaultTexture.PropertyName, skinTexture); + + if (!skinTexture) { - var skinTexture = defaultSkin.GetTexture(currentTexture.name); - renderer.material.SetTexture(textureID, skinTexture.TextureData); + // demo bogies et al. don't have textures... + renderer.material.color = defaultMaterial.color; + } - if (textureID == PropNames.MetalGlossMap) + if (defaultTexture.PropertyName == PropNames.MetalGlossMap) + { + if (!GetMaterialTexture(renderer.material, PropNames.OcclusionMap)) { - if (!GetMaterialTexture(renderer.sharedMaterial, PropNames.OcclusionMap)) - { - renderer.material.SetTexture(PropNames.OcclusionMap, skinTexture.TextureData); - } + renderer.material.SetTexture(PropNames.OcclusionMap, skinTexture); } } } diff --git a/SkinManagerMod/ThemeSettings.cs b/SkinManagerMod/ThemeSettings.cs index 9857885..2cc148c 100644 --- a/SkinManagerMod/ThemeSettings.cs +++ b/SkinManagerMod/ThemeSettings.cs @@ -13,7 +13,7 @@ public class ThemeSettings public bool PreventRandomSpawning; public float? CanPrice; - public SkinTexture CanLabel; + public SkinTexture? CanLabel; public Color? LabelBaseColor; public Color? LabelAccentColorA; public Color? LabelAccentColorB; @@ -50,7 +50,7 @@ private static void ReplaceIfNewer(ref T original, T other, bool otherIsNewer public static ThemeSettings Create(string basePath, ThemeConfigItem data, Version version) { - var result = new ThemeSettings(data.Name, version) + var result = new ThemeSettings(data.Name!, version) { HideFromStores = data.HideFromStores, PreventRandomSpawning = data.PreventRandomSpawning, @@ -90,7 +90,7 @@ public static ThemeSettings Create(string basePath, ThemeConfigItem data, Versio return result; } - private static void TryParseColor(string value, string configPath, ref Color? result) + private static void TryParseColor(string? value, string configPath, ref Color? result) { if (string.IsNullOrEmpty(value)) return; diff --git a/SkinManagerMod/ThreadHelper.cs b/SkinManagerMod/ThreadHelper.cs index cf87623..c54cfbb 100644 --- a/SkinManagerMod/ThreadHelper.cs +++ b/SkinManagerMod/ThreadHelper.cs @@ -19,17 +19,14 @@ public void Update() { if (_toExecute.Count == 0) return; - if (_toExecute.TryDequeue(out Action action)) - { - action?.Invoke(); - } + _toExecute.Dequeue()?.Invoke(); } protected override void OnDestroy() { - while (_toExecute.TryDequeue(out Action action)) + while (_toExecute.Count > 0) { - action?.Invoke(); + _toExecute.Dequeue()?.Invoke(); } base.OnDestroy(); diff --git a/SkinManagerMod/Translations.cs b/SkinManagerMod/Translations.cs index 31dfc6c..9384c2b 100644 --- a/SkinManagerMod/Translations.cs +++ b/SkinManagerMod/Translations.cs @@ -5,6 +5,8 @@ namespace SkinManagerMod { public static class Translations { + public static string LoadingScreen => L("skinman/ui/loading"); + // Comms Radio public static string ReskinMode => L("skinman/radio/repaint_mode"); public static string SelectCarPrompt => L("skinman/radio/select_car"); @@ -50,6 +52,7 @@ public static class DefaultSkinMode public static string PreferReskins => L("skinman/skinmode/prefer_reskins"); public static string AllowForCustomCars => L("skinman/skinmode/allow_custom"); public static string AllowForAllCars => L("skinman/skinmode/allow_all"); + public static string PreferDefaults => L("skinman/skinmode/prefer_default"); } } }