diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00c431d --- /dev/null +++ b/.gitignore @@ -0,0 +1,353 @@ +## 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/master/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/ +[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/ + +# 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 +*.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 + +# 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 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/ + +# 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/ +/Directory.Build.Props +/build +/.editorconfig diff --git a/JobHandler.cs b/JobHandler.cs new file mode 100644 index 0000000..42a74b3 --- /dev/null +++ b/JobHandler.cs @@ -0,0 +1,186 @@ +using DV; +using DV.Booklets; +using DV.Booklets.Rendered; +using DV.Common; +using DV.InventorySystem; +using DV.Logic.Job; +using DV.ServicePenalty; +using DV.ThingTypes; +using DV.Utils; +using HarmonyLib; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using UnityEngine; +using static HarmonyLib.AccessTools; + +namespace dvRadioJobControl +{ + public static class JobHandler + { + private static FieldRef> SpawnedJobOverviews = AccessTools.FieldRefAccess>("spawnedJobOverviews"); + private static Action? FoundJobOverviewAction; + + //code taken partially from vanilla + public static void HandInJob(Job job) + { + bool completed = false; + JobState newState = SingletonBehaviour.Instance.TryToCompleteAJob(job); + if (newState == JobState.Completed) completed = true; + + DisplayableDebt displayableDebt = (completed ? SingletonBehaviour.Instance.LastStagedJobDebt : SingletonBehaviour.Instance.GetExistingJobDebtForJob(job)); + if (displayableDebt != null && !displayableDebt.IsStaged) displayableDebt.UpdateDebtState(); + var jobReport = BookletCreator.CreateJobReport(job, displayableDebt, Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent); + CommsRadioController.PlayAudioFromRadio(JobValidator_Awake_Patch.ValidateSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobReport.gameObject)); + + if (completed) + { + JobBooklet b = JobBooklet.allExistingJobBooklets.FirstOrDefault(jb => jb.job.ID == job.ID); + SingletonBehaviour.Instance.DropItemFromHandsOrInventory(b.gameObject); + b.DestroyJobBooklet(); + SingletonBehaviour.Instance.AddMoney(job.GetWageForTheJob()); + CommsRadioController.PlayAudioFromRadio(MoneyPrinterJobValidator_Awake_Patch.MoneySound, PlayerManager.PlayerCamera.transform); + } + } + + private static IEnumerator CreateAndInitializePrintedObject(GameObject printedObject) + { + yield return null; + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.PrintingSound, PlayerManager.PlayerCamera.transform); + while ((!(printedObject.GetComponent()?.IsGenerated())) is true) yield return null; + + if (SingletonBehaviour.Instance && SingletonBehaviour.Instance.HasFreeSlots()) SingletonBehaviour.Instance.AddItemToInventory(printedObject); + else if (PlayerManager.PlayerCamera != null) + { + printedObject.transform.position = PlayerManager.PlayerCamera.transform.position + PlayerManager.PlayerCamera.transform.forward * 0.75f; + Rigidbody component = printedObject.GetComponent(); + if (component != null) + { + component.velocity = Vector3.zero; + component.angularVelocity = Vector3.zero; + } + + if ((bool)SingletonBehaviour.Instance) SingletonBehaviour.Instance.AddItemToWorldStorage(printedObject); + else UnityEngine.Debug.LogError("StorageController doesn't exist! Can't add item to WorldStorage!"); + } + else UnityEngine.Debug.LogError("Unexpected: No free space in inventory and PlayerCamera not found!"); + } + + public static void TryTakeOrDiscardJob(Job job, bool discard = false) + { + var sc = StationController.GetStationByYardID(job.ID.Split('-')[0]); + if (sc != null && sc.logicStation.availableJobs.Contains(job)) + { + FoundJobOverviewAction += TryAcceptJobOverview; + Main.RadioEntry.StartCoroutine(GetJobOverview(job, sc, discard)); + } + } + + private static IEnumerator GetJobOverview(Job job, StationController sc, bool discard = false) + { + SpawnedJobOverviews ??= AccessTools.FieldRefAccess>("spawnedJobOverviews"); + List? joList = SpawnedJobOverviews(sc); + if ((joList?.Any(jo => jo.job.ID == job.ID)) is not true) + { + joList = null; + StationJobGenerationRange_IsPlayerInRangeForBookletGeneration_Patch.scToOverride = sc; + yield return new WaitUntil(() => + { + Stopwatch st = Stopwatch.StartNew(); + joList = SpawnedJobOverviews(sc); + return ((joList is not null && joList is { Count: > 0 }) || st.ElapsedMilliseconds > 5000); + }); + } + var jobOverview = joList.FirstOrDefault(jo => jo.job.ID == job.ID); + if (discard) + { + if (jobOverview != null) + { + jobOverview.job.ExpireJob(); + jobOverview.DestroyJobOverview(); + } + } + else + { + FoundJobOverviewAction?.Invoke(jobOverview); + FoundJobOverviewAction -= TryAcceptJobOverview; + } + yield return new WaitForSeconds(2f); + StationJobGenerationRange_IsPlayerInRangeForBookletGeneration_Patch.scToOverride = null; + } + + private static void TryAcceptJobOverview(JobOverview jobOverview) + { + if (jobOverview == null) + { + UnityEngine.Debug.LogError("jobOverview is null, this shouldn´t have happened"); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + return; + } + + JobState state = jobOverview.job.State; + if (state != JobState.Available) + { + if (state != JobState.Expired) + { + UnityEngine.Debug.LogError(string.Concat(["Job[", jobOverview.job.ID, "] was already taken, but jobOverview: ", jobOverview.name, " was not destroyed! Destroying jobOverview"])); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + jobOverview.DestroyJobOverview(); + return; + } + var jobEReport = BookletCreator.CreateJobExpiredReport(jobOverview.job, Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobEReport.gameObject)); + jobOverview.DestroyJobOverview(); + return; + } + else + { + StationController stationController = StationController.allStations.FirstOrDefault((StationController st) => st.logicStation.availableJobs.Contains(jobOverview.job)); + if (!(stationController != null)) + { + UnityEngine.Debug.LogError("Job[" + jobOverview.job.ID + "] is in available state, but is not part of any station! Destroying jobOverview"); + jobOverview.DestroyJobOverview(); + return; + } + Job job = jobOverview.job; + if (!GameFeatureFlags.IsAllowed(GameFeatureFlags.Flag.UseJobValidator)) + { + var jobTWReport = BookletCreator.CreateTutorialWarningReport(Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobTWReport.gameObject)); + return; + } + if (SingletonBehaviour.Instance.currentJobs.Count >= SingletonBehaviour.Instance.GetNumberOfAllowedConcurrentJobs()) + { + var jobMLReport = BookletCreator.CreateMissingLicenseReport(job, false, Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobMLReport.gameObject)); + return; + } + if (!SingletonBehaviour.Instance.IsLicensedForJob(JobLicenseType_v2.ToV2List(job.requiredLicenses))) + { + var jobML2Report = BookletCreator.CreateMissingLicenseReport(job, true, Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobML2Report.gameObject)); + return; + } + if (!SingletonBehaviour.Instance.IsPlayerAllowedToTakeJob()) + { + var jobDWReport = BookletCreator.CreateDebtWarningReport(Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent); + CommsRadioController.PlayAudioFromRadio(PrinterController_Awake_Patch.ErrorSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobDWReport.gameObject)); + return; + } + stationController.TakeJobFromStation(jobOverview); + var jobReport = BookletCreator.CreateJobBooklet(job, Vector3.zero, Quaternion.identity, WorldMover.OriginShiftParent, true); + CommsRadioController.PlayAudioFromRadio(JobValidator_Awake_Patch.ValidateSound, PlayerManager.PlayerCamera.transform); + Main.RadioEntry.StartCoroutine(CreateAndInitializePrintedObject(jobReport.gameObject)); + return; + } + } + } +} diff --git a/Main.cs b/Main.cs index 3158d5a..da23b96 100644 --- a/Main.cs +++ b/Main.cs @@ -1,27 +1,54 @@ -using DV; +using CommsRadioAPI; using HarmonyLib; using System.Reflection; +using UnityEngine; using UnityModManagerNet; +using static UnityModManagerNet.UnityModManager; -namespace dvRadioJobControl; - -public static class Main +namespace dvRadioJobControl { - // Unity Mod Manage Wiki: https://wiki.nexusmods.com/index.php/Category:Unity_Mod_Manager - private static bool Load(UnityModManager.ModEntry modEntry) + [EnableReloading] + public static class Main { - modEntry.OnToggle = OnToggle; - return true; - } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + public static UnityModManager.ModEntry ModEntry { get; set; } + public static Harmony Harmony { get; set; } + public static CommsRadioMode RadioEntry { get; set; } +#pragma warning restore CS8618 - private static bool OnToggle(UnityModManager.ModEntry modEntry, bool active) - { - Harmony harmony = new Harmony(modEntry.Info.Id); - if (active) { - harmony.PatchAll(Assembly.GetExecutingAssembly()); - } else { - harmony.UnpatchAll(modEntry.Info.Id); + private static bool Load(UnityModManager.ModEntry modEntry) + { + ModEntry = modEntry; + ModEntry.OnToggle = OnToggle; + ModEntry.OnUnload = OnUnload; + return true; + } + + private static bool OnToggle(UnityModManager.ModEntry modEntry, bool active) + { + Harmony = new(modEntry.Info.Id); + if (active) + { + Harmony.PatchAll(Assembly.GetExecutingAssembly()); + ControllerAPI.Ready += SetupCommsRadioMenu; + } + else + { + Harmony.UnpatchAll(modEntry.Info.Id); + } + return true; + } + + private static bool OnUnload(UnityModManager.ModEntry modEntry) + { + Harmony.UnpatchAll(modEntry.Info.Id); + return true; + } + + private static void SetupCommsRadioMenu() + { + RadioEntry = CommsRadioMode.Create(new Menus.MainMenu(), new UnityEngine.Color(255, 123, 8, 0.75f)); + ControllerAPI.Ready -= SetupCommsRadioMenu; } - return true; } -} +} diff --git a/Menus.cs b/Menus.cs new file mode 100644 index 0000000..830be72 --- /dev/null +++ b/Menus.cs @@ -0,0 +1,437 @@ +using CommandTerminal; +using CommsRadioAPI; +using DV; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace dvRadioJobControl +{ + public class Menus + { + public static readonly LinkedList menus = new(); + public static LinkedListNode? currentMenu; + + public class MainMenu : AStateBehaviour + { + private TrainCar? hoveredCar; + + public MainMenu() + : base(new CommsRadioState( + titleText: "Radio Job Control", + contentText: "Manage job of selected car", + buttonBehaviour: ButtonBehaviourType.Regular)) + { } + + public override AStateBehaviour OnAction(CommsRadioUtility utility, InputAction action) + { + if (action != InputAction.Activate) + { + return this; + } + + if (BuildMenuItemList(hoveredCar)) + { + utility.PlaySound(VanillaSoundCommsRadio.ModeEnter); + return new ScrollableMenuItem(menus.First!.Value); + } + + Debug.LogWarning("Could not create menu list, something is very wrong!"); + return this; + } + + public override AStateBehaviour OnUpdate(CommsRadioUtility utility) + { + hoveredCar = Physics.Raycast(utility.SignalOrigin.position, utility.SignalOrigin.forward, out RaycastHit hit, 100f, LayerMask.GetMask("Train_Big_Collider")) + ? TrainCar.Resolve(hit.transform.root) + : null; + + return this; + } + + public bool BuildMenuItemList(TrainCar? startingCar) + { + menus.Clear(); + List possibleOps = []; + + if (startingCar != null) + { + Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(startingCar.logicCar); + if (jobOfCar != null) + { + possibleOps.Add(CarHandler.Operation.Validate); + possibleOps.Add(CarHandler.Operation.Discard); + possibleOps.Add(CarHandler.Operation.Exit); + } + else + { + possibleOps.Add(CarHandler.Operation.Reassign); + possibleOps.Add(CarHandler.Operation.Exit); + } + } + else + { + possibleOps.Add(CarHandler.Operation.Exit); + } + + Action leaveAction = (utility, nextState) => { if (nextState is not ScrollableMenuItem) CarHandler.Clear(); }; + + if (possibleOps.Contains(CarHandler.Operation.Reassign)) menus.AddLast(new MenuHolder( + CarHandler.Operation.Reassign, + "Reassign consist of car", + "", + ButtonBehaviourType.Override, + (utility, action, state) => CarHandler.Instance?.OnAction(utility, action, state) ?? state, + (utility, state) => CarHandler.Instance?.OnUpdate(utility, state) ?? state, + (utility, menuHolder) => CarHandler.EnsureCreated(utility, menuHolder), + leaveAction)); + + if (possibleOps.Contains(CarHandler.Operation.Validate)) menus.AddLast(new MenuHolder( + CarHandler.Operation.Validate, + "Validate job of consist", + "", + ButtonBehaviourType.Override, + (utility, action, state) => CarHandler.Instance?.OnAction(utility, action, state) ?? state, + (utility, state) => CarHandler.Instance?.OnUpdate(utility, state) ?? state, + (utility, menuHolder) => CarHandler.EnsureCreated(utility, menuHolder), + leaveAction)); + + if (possibleOps.Contains(CarHandler.Operation.Discard)) menus.AddLast(new MenuHolder( + CarHandler.Operation.Discard, + "Discard job of consist", + "", + ButtonBehaviourType.Override, + (utility, action, state) => CarHandler.Instance?.OnAction(utility, action, state) ?? state, + (utility, state) => CarHandler.Instance?.OnUpdate(utility, state) ?? state, + (utility, menuHolder) => CarHandler.EnsureCreated(utility, menuHolder), + leaveAction)); + + if (possibleOps.Contains(CarHandler.Operation.Exit)) menus.AddLast(new MenuHolder( + CarHandler.Operation.Exit, + "Click or scroll away", + "", + ButtonBehaviourType.Regular, + (_, __, ___) => new Menus.MainMenu(), + onLeave: leaveAction)); + + currentMenu = menus.First; + return true; + } + } + + public class ScrollableMenuItem : AStateBehaviour + { + public readonly MenuHolder currentMenu; + + public ScrollableMenuItem(MenuHolder currentMenu) + : base(new CommsRadioState( + currentMenu.TitleText.ToString(), + currentMenu.ContentText, + currentMenu.ActionText, + buttonBehaviour: currentMenu.ButtonBehaviour)) => this.currentMenu = currentMenu; + + public ScrollableMenuItem(CommsRadioState state, MenuHolder currentMenu) : base(state) => this.currentMenu = currentMenu; + + public override AStateBehaviour OnUpdate(CommsRadioUtility utility) => currentMenu.OnUpdate?.Invoke(utility, this) ?? this; + public override AStateBehaviour OnAction(CommsRadioUtility utility, InputAction action) => currentMenu.OnAction.Invoke(utility, action, this) ?? this; + + public override void OnEnter(CommsRadioUtility utility, AStateBehaviour? _) => currentMenu.OnEnter?.Invoke(utility, currentMenu); + + // Pass `next` state so we know if we are scrolling (keep tool active) or putting the radio away (clear tool) + public override void OnLeave(CommsRadioUtility utility, AStateBehaviour? next) => currentMenu.OnLeave?.Invoke(utility, next); + + public ScrollableMenuItem Copy() => new(state, currentMenu); + } + + public readonly record struct MenuHolder + { + public readonly CarHandler.Operation TitleText; + public readonly string ContentText; + public readonly string ActionText; + public readonly ButtonBehaviourType ButtonBehaviour; + public readonly Func OnAction; + public readonly Func? OnUpdate; + public readonly Action? OnEnter; + public readonly Action? OnLeave; + + public MenuHolder(CarHandler.Operation titleText, string contentText, string actionText, ButtonBehaviourType buttonBehaviour, Func onAction, Func? onUpdate = default, Action? onEnter = default, Action? onLeave = default) + { + TitleText = titleText; + ContentText = contentText; + ActionText = actionText; + ButtonBehaviour = buttonBehaviour; + OnAction = onAction; + OnUpdate = onUpdate; + OnEnter = onEnter; + OnLeave = onLeave; + } + } + } + + public class CarHandler + { + public static CarHandler? Instance; + + public TrainCar? CurrentCar; + public Job? CurrentJob; + public CommsRadioUtility Utility; + public Material Material; + public Menus.MenuHolder? MenuHolder; + + public CommsRadioCarDeleter? CarDeleter; + public GameObject? DeleterHighlighter; + public int? LAYER_MASK; + + public CarHandler(TrainCar? currentCar, CommsRadioUtility utility) + { + CurrentCar = currentCar; + Utility = utility; + Material = Utility.GetMaterial(VanillaMaterial.Valid)!; + } + + public static void EnsureCreated(CommsRadioUtility utility, Menus.MenuHolder menuHolder) + { + Instance ??= new CarHandler(null, utility); + Instance.MenuHolder = menuHolder; + Instance.LAYER_MASK ??= LayerMask.GetMask("Train_Big_Collider"); + + if (menuHolder.TitleText != Operation.Exit) Instance.RefreshCarDeleterComponent(); + else Instance.StopHighlight(); + } + + public static void Clear() + { + Instance?.StopHighlight(); + Instance = null; + } + + public AStateBehaviour OnAction(CommsRadioUtility utility, InputAction action, AStateBehaviour state) + { + switch (action) + { + case InputAction.Down: + { + Menus.currentMenu = Menus.currentMenu?.Next ?? Menus.menus.First; + var nextState = new Menus.ScrollableMenuItem(Menus.currentMenu!.Value); + Utility.PlaySound(VanillaSoundCommsRadio.ModeEnter); + return OnUpdate(utility, nextState); + } + + case InputAction.Up: + { + Menus.currentMenu = Menus.currentMenu?.Previous ?? Menus.menus.Last; + var nextState = new Menus.ScrollableMenuItem(Menus.currentMenu!.Value); + Utility.PlaySound(VanillaSoundCommsRadio.ModeEnter); + return OnUpdate(utility, nextState); + } + + case InputAction.Activate: + { + switch (MenuHolder?.TitleText) + { + case Operation.Exit: + Clear(); + utility.PlaySound(VanillaSoundCommsRadio.Cancel); + return new Menus.MainMenu(); + case Operation.Reassign: + TryReassignPersistentJobs(); + utility.PlaySound(VanillaSoundCommsRadio.Confirm); + break; + case Operation.Validate: + Debug.Log("Validation here"); + TryHandleJob(); + utility.PlaySound(VanillaSoundCommsRadio.Confirm); + break; + case Operation.Discard: + Debug.Log("Discard here"); + DiscardJob(); + utility.PlaySound(VanillaSoundCommsRadio.Confirm); + break; + } + + return (state as Menus.ScrollableMenuItem)!.Copy(); + } + default: + return state; + } + } + + public AStateBehaviour OnUpdate(CommsRadioUtility utility, AStateBehaviour state) + { + if (RaycastToCar()) + { + switch (MenuHolder?.TitleText) + { + case Operation.Reassign: + { + string rcontent = $"Car: {(CurrentCar is null ? "none" : CurrentCar.ID)} \nJob: {(CurrentJob is null ? "none" : CurrentJob.ID)}"; + var newrState = state.state.Fork(contentText: rcontent, actionText: "Reassign"); + return new Menus.ScrollableMenuItem(newrState, MenuHolder.Value); + } + + case Operation.Validate: + { + string vcontent = $"Car: {(CurrentCar is null ? "none" : CurrentCar.ID)} \nJob: {(CurrentJob is null ? "none" : CurrentJob.ID + $"\nState: {(CurrentJob.State)}")}"; + var newvState = state.state.Fork(contentText: vcontent, actionText: "Validate"); + return new Menus.ScrollableMenuItem(newvState, MenuHolder.Value); + } + + case Operation.Discard: + { + string dcontent = $"Car: {(CurrentCar is null ? "none" : CurrentCar.ID)} \nJob: {(CurrentJob is null ? "none" : CurrentJob.ID + $"\nState: {(CurrentJob.State)}")}"; + var newdState = state.state.Fork(contentText: dcontent, actionText: "Discard"); + return new Menus.ScrollableMenuItem(newdState, MenuHolder.Value); + } + + default: + return state; + } + } + + return state; + } + + public bool RaycastToCar() + { + bool retBool = false; + int mask = LAYER_MASK ?? LayerMask.GetMask("Train_Big_Collider"); + + TrainCar? newCar = Physics.Raycast(Utility.SignalOrigin.position, Utility.SignalOrigin.forward, out RaycastHit hit, 100f, mask) + ? TrainCar.Resolve(hit.transform.root) + : null; + + if (CurrentCar != newCar) + { + retBool = true; + CurrentCar = newCar; + CurrentJob = CurrentCar is null ? null : SingletonBehaviour.Instance.GetJobOfCar(CurrentCar!.logicCar); + if (CurrentCar != null) Utility.PlaySound(VanillaSoundCommsRadio.HoverOver); + HighlightCurrentCar(); + } + + return retBool; + } + + private void TryReassignPersistentJobs() + { + if (CurrentCar == null) return; + CommandArg carId = new() { String = CurrentCar.ID.ToString() }; + PersistentJobsMod.Console.RegenerateJobsForConsistOfCar([carId]); + } + + private void DiscardJob() + { + if (CurrentJob == null) return; + JobState jobState = CurrentJob.State; + switch (jobState) + { + case JobState.Available: + JobHandler.TryTakeOrDiscardJob(CurrentJob); + break; + case JobState.InProgress: + SingletonBehaviour.Instance.AbandonJob(CurrentJob); + break; + default: + Debug.LogError("Undiscardable job state" + jobState.ToString()); + break; + } + } + + private void TryHandleJob() + { + if (CurrentJob == null) return; + JobState jobState = CurrentJob.State; + switch (jobState) + { + case JobState.Available: + { + Debug.Log("Acceptance logic here"); + JobHandler.TryTakeOrDiscardJob(CurrentJob); + break; + } + case JobState.InProgress: + { + Debug.Log("Validation logic here"); + JobHandler.HandInJob(CurrentJob); + break; + } + default: + { + Debug.LogError("Unvalidatable job state" + jobState.ToString()); + break; + } + } + } + + private void HighlightCurrentCar() + { + if (CurrentCar == null) + { + StopHighlight(); + return; + } + + if (DeleterHighlighter == null) RefreshCarDeleterComponent(); + + if (DeleterHighlighter != null) + { + DeleterHighlighter.transform.localScale = CurrentCar.Bounds.size + new Vector3(0.25f, 0.8f, 0f); + Vector3 b = CurrentCar.transform.up * (DeleterHighlighter.transform.localScale.y / 2f); + Vector3 b2 = CurrentCar.transform.forward * CurrentCar.Bounds.center.z; + Vector3 position = CurrentCar.transform.position + b + b2; + + DeleterHighlighter.transform.SetPositionAndRotation(position, CurrentCar.transform.rotation); + DeleterHighlighter.SetActive(true); + DeleterHighlighter.transform.SetParent(CurrentCar.transform, true); + } + } + + public void StopHighlight() + { + if (DeleterHighlighter != null && DeleterHighlighter.activeSelf) + { + DeleterHighlighter.SetActive(false); + DeleterHighlighter.transform.SetParent(null); + } + } + + public void RefreshCarDeleterComponent() + { + if (DeleterHighlighter != null) return; + + LAYER_MASK = LayerMask.GetMask("Train_Big_Collider"); + + ICommsRadioMode? commsRadioMode = ControllerAPI.GetVanillaMode(VanillaMode.Clear); + if (commsRadioMode == null) + { + Debug.LogError("Could not find CommsRadioCarDeleter"); + return; + } + CarDeleter = (CommsRadioCarDeleter)commsRadioMode; + + DeleterHighlighter = CarDeleter.trainHighlighter; + if (DeleterHighlighter != null) + { + DeleterHighlighter.SetActive(false); + DeleterHighlighter.transform.SetParent(null); + + MeshRenderer highlighterRenderer = DeleterHighlighter.GetComponentInChildren(true); + if (highlighterRenderer != null) + { + highlighterRenderer.material = Material; + } + } + } + + public enum Operation + { + Exit, + Validate, + Discard, + Reassign + } + } +} \ No newline at end of file diff --git a/Patch.cs b/Patch.cs index 4ebc275..1ebfd76 100644 --- a/Patch.cs +++ b/Patch.cs @@ -75,4 +75,59 @@ static void Postfix(CommsRadioController __instance, List ___al ___allModes.Add(jobControl); } } + + //set booklet distance slightly smaller than job distance to ensure the booklets exist no matter where in the station the player is, so the mod can accept a job + [HarmonyPatch(typeof(StationJobGenerationRange), "Awake")] + public static class StationJobGenerationRange_Awake_Patch + { + static void Postfix(StationJobGenerationRange __instance) + { + __instance.jobOverviewBookletGenerationSqrDistance = __instance.generateJobsSqrDistance - 1000f; + } + } + + [HarmonyPatch(typeof(StationJobGenerationRange), "IsPlayerInRangeForBookletGeneration")] + public static class StationJobGenerationRange_IsPlayerInRangeForBookletGeneration_Patch + { + public static StationController? scToOverride = null; + static void Postfix(StationController __instance, ref bool __result) + { + if (__instance == scToOverride) __result = true; + } + } + + [HarmonyPatch(typeof(MoneyPrinterJobValidator), "Awake")] + public static class MoneyPrinterJobValidator_Awake_Patch + { + public static AudioClip? MoneySound; + + static void Postfix(MoneyPrinterJobValidator __instance) + { + MoneySound ??= Traverse.Create(__instance).Field("moneyOutputSound")?.GetValue(); + } + } + + [HarmonyPatch(typeof(JobValidator), "Awake")] + public static class JobValidator_Awake_Patch + { + public static AudioClip? ValidateSound; + + static void Postfix(JobValidator __instance) + { + ValidateSound ??= Traverse.Create(__instance).Field("jobValidatedSound")?.GetValue(); + } + } + + [HarmonyPatch(typeof(PrinterController), "Awake")] + public static class PrinterController_Awake_Patch + { + public static AudioClip? PrintingSound; + public static AudioClip? ErrorSound; + + static void Postfix(PrinterController __instance) + { + PrintingSound ??= Traverse.Create(__instance).Field("printingSound")?.GetValue(); + ErrorSound ??= Traverse.Create(__instance).Field("ErrorSound")?.GetValue(); + } + } } diff --git a/RadioJobControl.cs b/RadioJobControl.cs index b949d6d..d8dea9e 100644 --- a/RadioJobControl.cs +++ b/RadioJobControl.cs @@ -2,11 +2,13 @@ using DV; using DV.Booklets; using DV.Logic.Job; +using DV.ServicePenalty; using DV.ThingTypes; using DV.Utils; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Unity.Jobs; using UnityEngine; using UnityModManagerNet; @@ -137,7 +139,7 @@ public void OnUse() if (PointedCar != null) { CommsRadioController.PlayAudioFromRadio(selectedCarSound, transform); SetState(State.SelectCar); - Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(PointedCar); + Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(PointedCar.logicCar); if (jobOfCar == null) { selectedAction = PersistentJobsInstalled ? Act.reassign : Act.exit; } else { @@ -148,11 +150,11 @@ public void OnUse() break; case State.SelectCar: if (PointedCar != null) { - Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(PointedCar); + Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(PointedCar.logicCar); switch (selectedAction) { case Act.accept: - if (jobOfCar != null && jobOfCar.State == JobState.Available) { + if (IsValidJob(jobOfCar)) { CommsRadioController.PlayAudioFromRadio(confirmSound, transform); //SingletonBehaviour.Instance.TakeJob(jobOfCar, true); @@ -162,6 +164,7 @@ public void OnUse() List spawnedJobOverviews = (List)typeof(StationController).GetField("spawnedJobOverviews", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(startingStation); JobOverview jobOverview = spawnedJobOverviews.Find(jo => jo.job == jobOfCar); + if (PersistentJobsInstalled) PersistentJobsMod.HarmonyPatches.JobValidators.JobValidator_ProcessJobOverview_Patch.ReserveSpacePJ(jobOfCar, out bool shuntingJobOnWarehouseTrack); startingStation.TakeJobFromStation(jobOverview); BookletCreator.CreateJobBooklet(jobOfCar, valueTuple.Item1, valueTuple.Item2, (Transform)null); @@ -229,7 +232,7 @@ private void UpdateDisplay() string displayText = "manage jobs remotely"; if (PointedCar != null) { displayText = "Car: " + PointedCar.ID.ToString(); - Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(PointedCar); + Job jobOfCar = SingletonBehaviour.Instance.GetJobOfCar(PointedCar.logicCar); if (jobOfCar != null) { displayText += "\nJob: " + jobOfCar.ID.ToString() + "\nState: " + jobOfCar.State.ToString(); @@ -251,7 +254,7 @@ private void UpdateDisplay() } private bool IsValidAction(Act action) { - bool hasJob = PointedCar != null && SingletonBehaviour.Instance.GetJobOfCar(PointedCar) != null; + bool hasJob = PointedCar != null && SingletonBehaviour.Instance.GetJobOfCar(PointedCar.logicCar) != null; switch (action) { case Act.reassign: return !hasJob && PersistentJobsInstalled; @@ -264,6 +267,18 @@ private bool IsValidAction(Act action) { return true; } + private bool IsValidJob(Job job) + { + if (job != null && job.State == JobState.Available) + { + if (SingletonBehaviour.Instance.currentJobs.Count >= SingletonBehaviour.Instance.GetNumberOfAllowedConcurrentJobs()) return false; + if (!SingletonBehaviour.Instance.IsLicensedForJob(JobLicenseType_v2.ToV2List(job.requiredLicenses))) return false; + if (!SingletonBehaviour.Instance.IsPlayerAllowedToTakeJob()) return false; + return true; + } + return false; + } + public bool ButtonACustomAction() { if (selectedAction != Act.complete) { diff --git a/dvRadioJobControl.csproj b/dvRadioJobControl.csproj index 48d42a8..dd5a2ef 100644 --- a/dvRadioJobControl.csproj +++ b/dvRadioJobControl.csproj @@ -1,56 +1,153 @@ - - - - net48 - dvRadioJobControlMod - Disable Steam or Diesel for more immersion! - 0.2.0 - true - latest - dvRadioJobControl - E:\Steam\steamapps\common\Derail Valley\DerailValley_Data\Managed - - - - - $(AssemblySearchPaths); - $(ReferencePath); - - - - - enable - - - - - - - - - + + + + net481 + dvRadioJobControlMod + 0.2.0 + true + 12 + dvRadioJobControl + + + + + $(AssemblySearchPaths); + $(ReferencePath); + $(DVInstallPath) + + + + + enable + + + + + + + + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + + + + + + - E:\Steam\SteamApps\common\Derail Valley\DerailValley_Data\Managed\Unity.TextMeshPro.dll - - - - - - - - - $(ReferencePath)\..\..\Mods\PersistentJobs\PersistentJobsMod.dll - - - - - - - - - - - - - - + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + + + + $(DVInstallPath)\Mods\dvRadioJobControl\ + $(DeployDir)$(TargetFileName) + $(DeployDir)$(TargetName).pdb + $(DeployDll).OLD + + + + + + + + + + + + + + + + + +