Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LethalFaceTime v1.8.0
LethalFaceTime.dll
Decompiled a month ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using LethalCompanyInputUtils.Api; using LethalFaceTime.Bridges; using LethalFaceTime.Input; using LethalFaceTime.Runtime; using Microsoft.CodeAnalysis; using OpenBodyCams; using OpenBodyCams.API; using Scoops.gameobjects; using Scoops.misc; using Scoops.service; using TMPro; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.Animations.Rigging; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("LethalFaceTime")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Shows a live video feed of whoever you are on the phone with, on the phone screen.")] [assembly: AssemblyFileVersion("1.8.0.0")] [assembly: AssemblyInformationalVersion("1.8.0")] [assembly: AssemblyProduct("LethalFaceTime")] [assembly: AssemblyTitle("LethalFaceTime")] [assembly: AssemblyVersion("1.8.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace LethalFaceTime { internal sealed class FaceTimeConfig { public readonly ConfigEntry<bool> Enabled; public readonly ConfigEntry<int> ResolutionWidth; public readonly ConfigEntry<int> ResolutionHeight; public readonly ConfigEntry<float> Framerate; public readonly ConfigEntry<float> ScreenOpacity; public readonly ConfigEntry<float> ScreenInset; public readonly ConfigEntry<bool> DrawBehindPhoneWidgets; public readonly ConfigEntry<int> ScreenLayerOverride; public readonly ConfigEntry<bool> PauseFeedWhenPhoneStowed; public readonly ConfigEntry<TeardownMode> TeardownBehaviour; public readonly ConfigEntry<float> IdleKeepAliveSeconds; public readonly ConfigEntry<CameraFacing> DefaultFacing; public readonly ConfigEntry<float> FrontDistance; public readonly ConfigEntry<float> FrontHeightOffset; public readonly ConfigEntry<float> FrontLateralOffset; public readonly ConfigEntry<float> FrontYawAngle; public readonly ConfigEntry<float> FrontPitchAngle; public readonly ConfigEntry<float> PhoneFollowStrength; public readonly ConfigEntry<float> PhoneLensClearance; public readonly ConfigEntry<bool> MountCameraOnPhone; public readonly ConfigEntry<float> SelfieFieldOfView; public readonly ConfigEntry<float> RearForwardOffset; public readonly ConfigEntry<float> SubjectFraming; public readonly ConfigEntry<float> SwitchboardScreenOffset; public readonly ConfigEntry<bool> EnablePreview; public readonly ConfigEntry<ScreenCorner> PreviewCorner; public readonly ConfigEntry<float> PreviewSize; public readonly ConfigEntry<float> PreviewMargin; public readonly ConfigEntry<int> PreviewResolutionHeight; public readonly ConfigEntry<float> PreviewFramerate; public readonly ConfigEntry<bool> PreviewOnSwitchboard; public readonly ConfigEntry<bool> ShowOtherPlayersPhoneScreens; public readonly ConfigEntry<bool> RenderFacilityDuringCalls; public readonly ConfigEntry<bool> EnableSwitchboardMonitor; public readonly ConfigEntry<int> FullscreenResolutionWidth; public readonly ConfigEntry<int> FullscreenResolutionHeight; public readonly ConfigEntry<float> FullscreenFramerate; public readonly ConfigEntry<float> MonitorKnobSpacing; public readonly ConfigEntry<float> MonitorKnobScale; public readonly ConfigEntry<float> MonitorKnobOutwardOffset; public readonly ConfigEntry<bool> LogSwitchboardHierarchy; public readonly ConfigEntry<bool> HidePhoneWidgetsDuringCall; public readonly ConfigEntry<bool> HideFirstPersonPhoneFromCalls; public readonly ConfigEntry<bool> LetterboxPhoneVideo; public readonly ConfigEntry<Color> ScreenTint; public readonly ConfigEntry<float> ScreenTintStrength; public readonly ConfigEntry<float> ScreenEffectStrength; public readonly ConfigEntry<float> NightVisionStrength; public readonly ConfigEntry<float> SwitchboardScreenInset; public readonly ConfigEntry<bool> RequireBodyCamShipUpgrade; public readonly ConfigEntry<bool> MimicVoiceKeepsProximity; public readonly ConfigEntry<float> MimicVoiceProximityDistance; public readonly ConfigEntry<bool> InterferenceDegradesVideo; public readonly ConfigEntry<bool> ShowVideoForNonPlayerCallers; public readonly ConfigEntry<bool> ReleaseOrphanedRenderTextures; public readonly ConfigEntry<float> ReconcileInterval; public readonly ConfigEntry<bool> VerboseLogging; public Vector2Int Resolution => new Vector2Int(ResolutionWidth.Value, ResolutionHeight.Value); public Vector2Int FullscreenResolution { get { //IL_006f: Unknown result type (might be due to invalid IL or missing references) int value = FullscreenResolutionHeight.Value; int num = FullscreenResolutionWidth.Value; if (num <= 0) { float num2 = ((ResolutionHeight.Value > 0) ? ((float)ResolutionWidth.Value / (float)ResolutionHeight.Value) : 1.3333334f); num = Mathf.RoundToInt((float)value * num2); } return new Vector2Int(Mathf.Clamp(num, 64, 1920), Mathf.Clamp(value, 64, 1080)); } } public FaceTimeConfig(ConfigFile file) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Expected O, but got Unknown //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Expected O, but got Unknown //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Expected O, but got Unknown //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Expected O, but got Unknown //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Expected O, but got Unknown //IL_023a: Unknown result type (might be due to invalid IL or missing references) //IL_0244: Expected O, but got Unknown //IL_02e3: Unknown result type (might be due to invalid IL or missing references) //IL_02ed: Expected O, but got Unknown //IL_031c: Unknown result type (might be due to invalid IL or missing references) //IL_0326: Expected O, but got Unknown //IL_034f: Unknown result type (might be due to invalid IL or missing references) //IL_0359: Expected O, but got Unknown //IL_0388: Unknown result type (might be due to invalid IL or missing references) //IL_0392: Expected O, but got Unknown //IL_0415: Unknown result type (might be due to invalid IL or missing references) //IL_041f: Expected O, but got Unknown //IL_046a: Unknown result type (might be due to invalid IL or missing references) //IL_0474: Expected O, but got Unknown //IL_04a3: Unknown result type (might be due to invalid IL or missing references) //IL_04ad: Expected O, but got Unknown //IL_04dc: Unknown result type (might be due to invalid IL or missing references) //IL_04e6: Expected O, but got Unknown //IL_0515: Unknown result type (might be due to invalid IL or missing references) //IL_051f: Expected O, but got Unknown //IL_054e: Unknown result type (might be due to invalid IL or missing references) //IL_0558: Expected O, but got Unknown //IL_0587: Unknown result type (might be due to invalid IL or missing references) //IL_0591: Expected O, but got Unknown //IL_05c0: Unknown result type (might be due to invalid IL or missing references) //IL_05ca: Expected O, but got Unknown //IL_05f9: Unknown result type (might be due to invalid IL or missing references) //IL_0603: Expected O, but got Unknown //IL_0632: Unknown result type (might be due to invalid IL or missing references) //IL_063c: Expected O, but got Unknown //IL_066b: Unknown result type (might be due to invalid IL or missing references) //IL_0675: Expected O, but got Unknown //IL_06b8: Unknown result type (might be due to invalid IL or missing references) //IL_06c2: Expected O, but got Unknown //IL_06ee: Unknown result type (might be due to invalid IL or missing references) //IL_06f8: Expected O, but got Unknown //IL_0727: Unknown result type (might be due to invalid IL or missing references) //IL_0731: Expected O, but got Unknown //IL_0760: Unknown result type (might be due to invalid IL or missing references) //IL_076a: Expected O, but got Unknown //IL_0799: Unknown result type (might be due to invalid IL or missing references) //IL_07a3: Expected O, but got Unknown //IL_07d2: Unknown result type (might be due to invalid IL or missing references) //IL_07dc: Expected O, but got Unknown //IL_086c: Unknown result type (might be due to invalid IL or missing references) //IL_08aa: Unknown result type (might be due to invalid IL or missing references) //IL_08b4: Expected O, but got Unknown //IL_08e3: Unknown result type (might be due to invalid IL or missing references) //IL_08ed: Expected O, but got Unknown //IL_091c: Unknown result type (might be due to invalid IL or missing references) //IL_0926: Expected O, but got Unknown //IL_0955: Unknown result type (might be due to invalid IL or missing references) //IL_095f: Expected O, but got Unknown Enabled = file.Bind<bool>("General", "Enabled", true, "Master switch. When false the mod never creates a camera and never touches the phone model."); ReconcileInterval = file.Bind<float>("General", "ReconcileInterval", 0.25f, new ConfigDescription("Seconds between safety-net checks of the local phone's call state. LethalPhones exposes no call events, so the mod hooks PlayerPhone.UpdateCallingUI for instant reaction and uses this low-frequency poll to catch anything the hook misses (disconnects, round end, ownership loss).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 2f), Array.Empty<object>())); VerboseLogging = file.Bind<bool>("General", "VerboseLogging", false, "Log every call/camera state transition. Useful when reporting a bug."); ResolutionWidth = file.Bind<int>("Video", "ResolutionWidth", 256, new ConfigDescription("Width of the video feed in pixels. OpenBodyCams' own default is 160x120; a phone screen is small so there is little reason to go high.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(32, 1024), Array.Empty<object>())); ResolutionHeight = file.Bind<int>("Video", "ResolutionHeight", 192, new ConfigDescription("Height of the video feed in pixels.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(32, 1024), Array.Empty<object>())); Framerate = file.Bind<float>("Video", "Framerate", 24f, new ConfigDescription("Frames per second the phone camera renders at. This maps onto OpenBodyCams' own frame limiter, so lowering it is a direct, real performance saving. 0 means 'render every frame'.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f), Array.Empty<object>())); ScreenOpacity = file.Bind<float>("Screen", "Opacity", 1f, new ConfigDescription("Opacity of the video layer on the phone screen.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 1f), Array.Empty<object>())); ScreenInset = file.Bind<float>("Screen", "Inset", 0f, new ConfigDescription("Padding in canvas units between the edge of the phone's screen canvas and the video. Increase if the video overlaps artwork you want to keep visible.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 200f), Array.Empty<object>())); DrawBehindPhoneWidgets = file.Bind<bool>("Screen", "DrawBehindPhoneWidgets", true, "True: the video is the bottom layer of the phone canvas, so the dialled number, status text and signal icons stay readable on top of it. False: the video covers them."); ScreenLayerOverride = file.Bind<int>("Screen", "LayerOverride", -1, new ConfigDescription("-1 keeps the video on the same Unity layer as the phone's screen canvas (recommended). Set to 23 to put it on the layer OpenBodyCams itself uses to hide first-person geometry from body cams - do that only if you actually see a phone screen recursively mirrored inside a body cam feed.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(-1, 31), Array.Empty<object>())); PauseFeedWhenPhoneStowed = file.Bind<bool>("Performance", "PauseFeedWhenPhoneStowed", true, "Stop rendering the camera while the phone is flipped shut. The call stays connected; only the camera sleeps. Strongly recommended."); TeardownBehaviour = file.Bind<TeardownMode>("Performance", "TeardownBehaviour", TeardownMode.DisableOnly, "What to do with the OpenBodyCams camera when a call ends.\nDisableOnly - switch the camera off and keep the object for the rest of the session. Default, and recommended: disabling makes OpenBodyCams set Camera.enabled = false, which is a true zero per-frame cost, so the only thing retained is one small render texture. Every call after the first is also instant.\nDisableThenDestroyWhenIdle - as above, then destroy the camera after IdleKeepAliveSeconds.\nDestroyImmediately - free everything the moment the call drops. Lowest idle memory, but the next call pays to rebuild the camera.\nNote on the two destroying modes: OpenBodyCams creates one HDRP/Unlit material per camera and never destroys it, and that field is internal so this mod cannot destroy it either. Each rebuild therefore leaks one small material. Harmless over a few calls, but it is why DisableOnly is the default rather than the tidiest-looking option."); IdleKeepAliveSeconds = file.Bind<float>("Performance", "IdleKeepAliveSeconds", 30f, new ConfigDescription("Only used by DisableThenDestroyWhenIdle.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 600f), Array.Empty<object>())); InterferenceDegradesVideo = file.Bind<bool>("Flavour", "InterferenceDegradesVideo", true, "Fade the video towards black as LethalPhones' own connection interference rises, so a bad line looks as bad as it sounds."); DefaultFacing = file.Bind<CameraFacing>("Camera", "DefaultFacing", CameraFacing.Front, "Which way the phone camera points when a call starts.\nFront - the selfie view: the camera floats at arm's length in front of the other person, looking back at them, so it reads as though they are holding the phone up.\nRear - their point of view, i.e. an ordinary body cam looking out at whatever they are looking at."); EnablePreview = file.Bind<bool>("Preview", "EnablePreview", true, "Show a small picture of what your end of the call is transmitting, in the corner of the screen, the way a video call shows you your own face. On the switchboard it shows the switchboard's own camera. Costs one extra camera per screen; turn it off if you are short on frames."); PreviewCorner = file.Bind<ScreenCorner>("Preview", "PreviewCorner", ScreenCorner.BottomRight, "Which corner the preview starts in. Move it in-game with the 'Move Phone Preview' keybind, or - at the switchboard - the previous/next knobs while the caller view is up. Both changes are shared with the rest of the lobby, so this is only the starting value."); PreviewSize = file.Bind<float>("Preview", "PreviewSize", 0.3f, new ConfigDescription("How much of the screen's shorter side the self-view takes up.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 0.6f), Array.Empty<object>())); PreviewMargin = file.Bind<float>("Preview", "PreviewMargin", 0.04f, new ConfigDescription("Gap between the preview and the edges of the screen, as a fraction of the screen's shorter side.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.25f), Array.Empty<object>())); PreviewResolutionHeight = file.Bind<int>("Preview", "PreviewResolutionHeight", 96, new ConfigDescription("Vertical resolution of the preview feed. It is a thumbnail, so this can stay small - the width follows the main feed's aspect ratio.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(32, 480), Array.Empty<object>())); PreviewFramerate = file.Bind<float>("Preview", "PreviewFramerate", 15f, new ConfigDescription("How often the preview redraws, in frames per second. Lower than the main feed on purpose: it is a confidence monitor, not the thing you are watching.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), Array.Empty<object>())); RenderFacilityDuringCalls = file.Bind<bool>("Video", "RenderFacilityDuringCalls", true, "Keep the facility interior rendered while a call is filming somebody inside it.\nLethal Company culls the interior per tile from ONE viewpoint - the local player - so with you outside, the whole dungeon is switched off and no camera can see it. That is why the facility only appeared once both callers were inside, and vanished when you left. This suspends that culling while such a call is up, which means the interior is not occlusion-culled for anyone during the call. Turn it off if that costs you more frames than the picture is worth."); ShowOtherPlayersPhoneScreens = file.Bind<bool>("Preview", "ShowOtherPlayersPhoneScreens", true, "Draw other players' phone screens on the handsets in their hands, so standing next to someone on a call shows you what is on their phone. The video cannot be sent over the network, so your machine renders it: two extra cameras per handset you can actually see (their far end, plus their corner preview). Nothing renders for a phone that is off-camera or flipped shut. Turn this off if a ship full of simultaneous calls costs you frames."); PreviewOnSwitchboard = file.Bind<bool>("Preview", "PreviewOnSwitchboard", true, "Also show a preview of the switchboard's own camera on its screen while the caller view is showing a player. Its corner is moved with the switchboard's previous/next knobs, and the move is shared with everyone in the lobby."); PhoneFollowStrength = file.Bind<float>("Camera", "PhoneFollowStrength", 0.65f, new ConfigDescription("How much the camera rides the handset rather than the subject's look direction. The handset carries the walk bob and the hand animation, which is what makes the shot feel hand-held; the look direction keeps the face towards the lens even when the arm lags. 0 is a rigid head cam, 1 follows the hand completely.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); MountCameraOnPhone = file.Bind<bool>("Camera", "MountCameraOnPhone", true, "Bolts the camera to the handset itself, the way a real phone's front camera is bolted to its screen. This is what stops the phone ever getting in front of the lens - while walking, while looking around, and while holding the dial key, which raises the handset in front of the face. Turn it off for the old free-floating camera, which frames wider but can be blocked by the phone."); PhoneLensClearance = file.Bind<float>("Camera", "PhoneLensClearance", 0.14f, new ConfigDescription("How far off the handset's screen face the lens sits, in metres, when MountCameraOnPhone is on. Must clear the phone's own geometry: too small and the camera ends up inside the model, which renders as a solid black picture.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.02f, 0.5f), Array.Empty<object>())); SelfieFieldOfView = file.Bind<float>("Camera", "SelfieFieldOfView", 82f, new ConfigDescription("Vertical field of view for this mod's cameras, in degrees. Wider than OpenBodyCams' own 65 on purpose: with the camera bolted to a handset held near the ear it sits about 30 cm from the face, and field of view is the only way to widen that shot - backing the camera off would just put the phone in front of it again. Raise it for more head and shoulders, lower it for a tighter face.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 120f), Array.Empty<object>())); FrontDistance = file.Bind<float>("Camera", "FrontDistance", 0.65f, new ConfigDescription("Front camera only: how far in front of the head the camera sits, in metres. Roughly arm's length. Larger values show more of their body and surroundings.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.15f, 3f), Array.Empty<object>())); FrontHeightOffset = file.Bind<float>("Camera", "FrontHeightOffset", 0.05f, new ConfigDescription("Front camera only: vertical offset from head height, in metres. Slightly positive reads as a phone held just above eye line.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-1f, 1f), Array.Empty<object>())); FrontLateralOffset = file.Bind<float>("Camera", "FrontLateralOffset", -0.12f, new ConfigDescription("Front camera only: sideways offset in metres. Negative is towards their left hand, which is the hand LethalPhones puts the phone in - a small negative value is what makes the shot read as hand-held rather than a floating tripod.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-1f, 1f), Array.Empty<object>())); FrontYawAngle = file.Bind<float>("Camera", "FrontYawAngle", -8f, new ConfigDescription("Front camera only: how far round to the side the camera sits, in degrees. Negative puts it on the subject's left, which is the hand LethalPhones puts the phone in - that is what makes the shot read as hand-held rather than as a webcam bolted to their forehead. Positive swings it round to the right instead.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-80f, 80f), Array.Empty<object>())); FrontPitchAngle = file.Bind<float>("Camera", "FrontPitchAngle", -6f, new ConfigDescription("Front camera only: vertical angle in degrees. Negative sits below the eye line and looks slightly up, which is how a phone held at chest height sees you.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-70f, 70f), Array.Empty<object>())); RearForwardOffset = file.Bind<float>("Camera", "RearForwardOffset", 0.15f, new ConfigDescription("Rear camera only: how far ahead of the head the camera sits, so their own body does not clip into frame.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>())); SubjectFraming = file.Bind<float>("Camera", "SubjectFraming", 1f, new ConfigDescription("Framing multiplier for non-player callers - creatures holding a phone. The camera pulls back by a multiple of the subject's own measured size, so a knee-high lootbug and a man-sized masked enemy both frame sensibly from one number. Raise it to sit further away and show more of the room.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.2f, 5f), Array.Empty<object>())); SwitchboardScreenOffset = file.Bind<float>("Camera", "SwitchboardScreenOffset", 0f, new ConfigDescription("Extra distance in metres that the switchboard camera stands off from the centre of its screen. The mod already steps far enough out to avoid clipping into the mesh; raise this if the shot still starts inside the cabinet.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>())); EnableSwitchboardMonitor = file.Bind<bool>("Switchboard", "EnableSwitchboardMonitor", true, "Adds a knob to the ship's switchboard, between its select-next and select-previous knobs, that toggles a view of whoever is currently calling the switchboard on the switchboard's own display. Purely local: it sends no network messages, so nobody else in the lobby needs this mod, and each player toggles their own view."); FullscreenResolutionWidth = file.Bind<int>("Switchboard", "FullscreenResolutionWidth", 0, new ConfigDescription("Width of the caller feed shown on the switchboard's display, or 0 to derive it from the framing (recommended). OpenBodyCams' field of view is VERTICAL, so a wider texture gains image at the sides rather than losing any at the top and bottom.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1920), Array.Empty<object>())); FullscreenResolutionHeight = file.Bind<int>("Switchboard", "FullscreenResolutionHeight", 480, new ConfigDescription("Height of the switchboard monitor feed.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 1080), Array.Empty<object>())); FullscreenFramerate = file.Bind<float>("Switchboard", "FullscreenFramerate", 30f, new ConfigDescription("Frames per second for the switchboard monitor feed. 0 renders every frame.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f), Array.Empty<object>())); MonitorKnobSpacing = file.Bind<float>("Switchboard", "MonitorKnobSpacing", 0f, new ConfigDescription("Where the new knob sits along the line joining the select-next and select-previous knobs. 0 is exactly halfway between them (the default); -1 and 1 sit on top of one or the other; values beyond that continue past them. The prefab's real positions live in an AssetBundle and are only measurable in game, so this is the dial to turn if it lands somewhere awkward.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-3f, 3f), Array.Empty<object>())); MonitorKnobScale = file.Bind<float>("Switchboard", "MonitorKnobScale", 0.28f, new ConfigDescription("Diameter of the visible knob as a fraction of the gap between the switchboard's select-next and select-previous knobs. Sized against that gap rather than in metres so it stays right whatever scale the switchboard is placed at.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 2f), Array.Empty<object>())); MonitorKnobOutwardOffset = file.Bind<float>("Switchboard", "MonitorKnobOutwardOffset", 0f, new ConfigDescription("Pushes the knob out of the panel along the switchboard's own forward axis, in metres. Raise this if the knob is buried in the cabinet - a knob whose collider is inside solid geometry cannot be interacted with, because the interact raycast hits the cabinet first.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-0.5f, 0.5f), Array.Empty<object>())); LogSwitchboardHierarchy = file.Bind<bool>("Switchboard", "LogSwitchboardHierarchy", true, "Dumps the switchboard's full child hierarchy to the log once - names, local positions, and which children have renderers or colliders. The switchboard prefab lives inside an AssetBundle, so this is the only way to see what is actually in there. Turn it on if the knob lands in the wrong place, and send the log. On by default for now, because the knob placement is still being dialled in; set it false once you are happy."); HidePhoneWidgetsDuringCall = file.Bind<bool>("Screen", "HidePhoneWidgetsDuringCall", true, "Take LethalPhones' own screen widgets - dialled number, call status, signal bars, volume icon - out of the picture entirely while video is playing, rather than just fading them. They are restored the moment the video stops, so the phone is still fully usable for dialling. Set false to leave them over the video."); HideFirstPersonPhoneFromCalls = file.Bind<bool>("Screen", "HideFirstPersonPhoneFromCalls", true, "Keeps your own held handset out of your outgoing picture, by removing its layer from this mod's cameras only. Every other camera - including your own eyes - still draws it normally. Turn it off if your phone stops rendering in your hands."); LetterboxPhoneVideo = file.Bind<bool>("Screen", "LetterboxPhoneVideo", true, "Fit the video inside the handset screen so the whole subject is visible, instead of filling the screen and cropping the sides off."); ScreenTint = file.Bind<Color>("Screen", "ScreenTint", new Color(0.42f, 1f, 0.55f), "The colour the feed is tinted towards, so it reads as something coming through a phone screen rather than a clean camera render. The default is a barely-there phosphor green."); ScreenTintStrength = file.Bind<float>("Screen", "ScreenTintStrength", 0.45f, new ConfigDescription("How far the picture is pushed towards ScreenTint. 0 is a completely clean render, 1 is the full tint colour. Applies to the handset and the switchboard alike.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); ScreenEffectStrength = file.Bind<float>("Screen", "ScreenEffectStrength", 0.18f, new ConfigDescription("Strength of the faint scanline overlay that sells the 'displayed on a screen' look. Deliberately low - it should be something you notice only if you look for it. 0 turns it off entirely.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); NightVisionStrength = file.Bind<float>("Screen", "NightVisionStrength", 0.4f, new ConfigDescription("Scales OpenBodyCams' night-vision light on this mod's cameras only, never on the ship monitor. That light is cloned from the player's own night vision, which is green, and at full strength it is most of why a body cam feed looks so heavily tinted. Lower means a more natural, less washed-out picture; 1 matches the ship monitor exactly. Dark interiors will be genuinely darker as you lower it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); SwitchboardScreenInset = file.Bind<float>("Switchboard", "SwitchboardScreenInset", 0f, new ConfigDescription("Padding in canvas units between the edge of the switchboard's display and the caller view drawn on it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 400f), Array.Empty<object>())); RequireBodyCamShipUpgrade = file.Bind<bool>("Compatibility", "RequireBodyCamShipUpgrade", false, "OpenBodyCams can put the ship's body cam behind a purchasable upgrade (its own ShipUpgradeEnabled option, active whenever LethalLib is installed). When this is true, the phone video feed waits on that same purchase.\nDefault false, because a phone call is a different feature from the ship monitor, and silently showing nothing until someone buys an antenna makes the mod look broken. Set it true if you would rather the phone respect the upgrade."); MimicVoiceKeepsProximity = file.Bind<bool>("Compatibility", "MimicVoiceKeepsProximity", true, "Mirage only. A creature mimicking a voice while it is on the phone is heard from where it is standing, as long as you are near it, instead of only through the handset.\nLethalPhones relays nearby sounds by taking over the sound's own AudioSource and making it play flat out of the earpiece, and it decides a sound is worth relaying from that source's falloff settings - which calls a voice inaudible from about ten metres. A masked enemy talking in the same room as you is well inside that, so its Mirage voice was leaving the room and arriving in your phone. Set this false to restore LethalPhones' own behaviour exactly."); MimicVoiceProximityDistance = file.Bind<float>("Compatibility", "MimicVoiceProximityDistance", 20f, "How near a mimicking creature has to be, in metres, for its voice to play from the creature rather than down the phone line. Beyond this the phone carries it as before, which is what makes a masked calling you from across the map work at all."); ShowVideoForNonPlayerCallers = file.Bind<bool>("Flavour", "ShowVideoForNonPlayerCallers", true, "Also show a feed when the other end of the call is not a player - the switchboard, or one of the creatures that can pick up a phone. Disable for players-only FaceTime."); ReleaseOrphanedRenderTextures = file.Bind<bool>("Advanced", "ReleaseOrphanedRenderTextures", true, "OpenBodyCams allocates a fresh RenderTexture every time a camera's settings change and does not release the old one. When this is enabled the mod releases the previous texture of its OWN camera (never anyone else's) to stop VRAM creeping up over a long session."); } } internal enum ScreenCorner { TopLeft, TopRight, BottomRight, BottomLeft } internal enum CameraFacing { Front, Rear } internal enum TeardownMode { DestroyImmediately, DisableThenDestroyWhenIdle, DisableOnly } public static class FaceTimeEvents { public static event Action<CallConnectedInfo> CallConnected; public static event Action<PlayerPhone> CallEnded; public static event Action<RenderTexture> VideoTextureChanged; internal static void RaiseCallConnected(CallConnectedInfo info) { Raise(FaceTimeEvents.CallConnected, info, "CallConnected"); } internal static void RaiseCallEnded(PlayerPhone localPhone) { Raise(FaceTimeEvents.CallEnded, localPhone, "CallEnded"); } internal static void RaiseVideoTextureChanged(RenderTexture texture) { Raise(FaceTimeEvents.VideoTextureChanged, texture, "VideoTextureChanged"); } private static void Raise<T>(Action<T> handler, T argument, string name) { if (handler == null) { return; } Delegate[] invocationList = handler.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action<T>)obj)(argument); } catch (Exception ex) { Plugin.Log.LogError((object)("A " + name + " subscriber threw: " + ex)); } } } } public readonly struct CallConnectedInfo { public readonly PlayerPhone LocalPhone; public readonly PhoneBehavior RemotePhone; public readonly short RemoteNumber; public readonly bool RemoteIsPlayer; internal CallConnectedInfo(PlayerPhone localPhone, PhoneBehavior remotePhone, short remoteNumber, bool remoteIsPlayer) { LocalPhone = localPhone; RemotePhone = remotePhone; RemoteNumber = remoteNumber; RemoteIsPlayer = remoteIsPlayer; } } [BepInPlugin("rayan.LethalFaceTime", "LethalFaceTime", "1.8.0")] [BepInDependency("LethalPhones", "1.3.0")] [BepInDependency("Zaggy1024.OpenBodyCams", "3.0.0")] [BepInDependency("com.rune580.LethalCompanyInputUtils", "0.7.0")] public sealed class Plugin : BaseUnityPlugin { private Harmony _harmony; private bool _loggedTickFailure; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static FaceTimeConfig Settings { get; private set; } internal static FaceTimeInput Keybinds { get; private set; } private void Awake() { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; try { Settings = new FaceTimeConfig(((BaseUnityPlugin)this).Config); ((BaseUnityPlugin)this).Config.SettingChanged += HandleSettingChanged; Keybinds = new FaceTimeInput(); _harmony = new Harmony("rayan.LethalFaceTime"); _harmony.PatchAll(typeof(Plugin).Assembly); FaceTimeManager.Create(Settings, (MonoBehaviour)(object)this); Log.LogInfo((object)"LethalFaceTime 1.8.0 loaded."); } catch (Exception ex) { Log.LogError((object)("Failed to initialise LethalFaceTime: " + ex)); } } private void Update() { try { FaceTimeManager.Instance?.Tick(); } catch (Exception ex) { if (!_loggedTickFailure) { _loggedTickFailure = true; Log.LogError((object)("LethalFaceTime's update loop threw; further occurrences suppressed: " + ex)); } } } private void LateUpdate() { try { FaceTimeManager.Instance?.LateTick(); } catch (Exception ex) { if (!_loggedTickFailure) { _loggedTickFailure = true; Log.LogError((object)("LethalFaceTime's late update threw; further occurrences suppressed: " + ex)); } } } private void OnDestroy() { try { FaceTimeManager.Instance?.Shutdown(); ((BaseUnityPlugin)this).Config.SettingChanged -= HandleSettingChanged; Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch (Exception ex) { Log.LogWarning((object)("Failed to unpatch cleanly: " + ex.Message)); } } private static void HandleSettingChanged(object sender, SettingChangedEventArgs args) { FaceTimeManager.NotifySettingsChanged(); } internal static void LogVerbose(string message) { if (Settings != null && Settings.VerboseLogging.Value) { Log.LogInfo((object)message); } } } internal static class PluginMetadata { public const string Guid = "rayan.LethalFaceTime"; public const string Name = "LethalFaceTime"; public const string Version = "1.8.0"; } internal static class Dependencies { public const string LethalPhonesGuid = "LethalPhones"; public const string LethalPhonesMinimumVersion = "1.3.0"; public const string OpenBodyCamsGuid = "Zaggy1024.OpenBodyCams"; public const string OpenBodyCamsMinimumVersion = "3.0.0"; public const string InputUtilsGuid = "com.rune580.LethalCompanyInputUtils"; public const string InputUtilsMinimumVersion = "0.7.0"; } } namespace LethalFaceTime.Runtime { internal sealed class FaceTimeFeed : IDisposable { private const string GeneratedMonitorMaterialName = "BodyCamMaterial"; private static readonly FieldInfo MonitorOnMaterialField = AccessTools.Field(typeof(BodyCamComponent), "MonitorOnMaterial"); private static readonly FieldInfo NightVisionLightField = AccessTools.Field(typeof(BodyCamComponent), "nightVisionLight"); private readonly FaceTimeConfig _config; private readonly MonoBehaviour _coroutineHost; private Vector2Int _resolution; private float _framerate; private float _baseNightVisionIntensity = -1f; private int _extraCullingLayers; private int _removedCullingLayers; private bool _warnedWorldLayer; private GameObject _host; private BodyCamComponent _bodyCam; private RenderTexture _texture; private bool _rendering = true; public bool IsDead { get { if (!((Object)(object)_bodyCam == (Object)null)) { return (Object)(object)_host == (Object)null; } return true; } } public RenderTexture Texture => _texture; public bool HasSignal { get { if (IsDead || (Object)(object)_texture == (Object)null || !_rendering) { return false; } try { return !_bodyCam.IsBlanked; } catch (Exception) { return false; } } } public Lens Lens { get { if (IsDead) { return new Lens(null); } try { return new Lens(_bodyCam.GetCamera()); } catch (Exception) { return new Lens(null); } } } public int CameraCullingMask { get { if (IsDead) { return 0; } try { Camera camera = _bodyCam.GetCamera(); return ((Object)(object)camera != (Object)null) ? camera.cullingMask : 0; } catch (Exception) { return 0; } } } public event Action<RenderTexture> TextureChanged; private static bool IsWorldLayer(int layer) { try { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } return (instance.collidersAndRoomMaskAndDefault & (1 << layer)) != 0; } catch (Exception) { return false; } } private FaceTimeFeed(FaceTimeConfig config, MonoBehaviour coroutineHost, Vector2Int resolution, float framerate) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) _config = config; _coroutineHost = coroutineHost; _resolution = resolution; _framerate = framerate; } public void AddCullingLayer(int layer) { if (IsDead || layer < 0 || layer > 31) { return; } _extraCullingLayers |= 1 << layer; try { Camera camera = _bodyCam.GetCamera(); if ((Object)(object)camera != (Object)null) { camera.cullingMask |= _extraCullingLayers; } } catch (Exception ex) { Plugin.LogVerbose("Could not extend the camera culling mask: " + ex.Message); } } public void RemoveCullingLayer(int layer) { if (IsDead || layer <= 0 || layer > 31) { return; } if (IsWorldLayer(layer)) { if (!_warnedWorldLayer) { _warnedWorldLayer = true; Plugin.Log.LogWarning((object)("Refusing to hide the handset from this feed: its renderers are on layer " + layer + " (" + LayerMask.LayerToName(layer) + "), which the level itself is built on. Removing it would stop the facility rendering on the phone. The handset will stay visible instead; set HideFirstPersonPhoneFromCalls=false to silence this.")); } return; } int num = 1 << layer; if ((_removedCullingLayers & num) != 0) { return; } _removedCullingLayers |= num; try { Camera camera = _bodyCam.GetCamera(); if ((Object)(object)camera != (Object)null) { camera.cullingMask &= ~_removedCullingLayers; } } catch (Exception ex) { Plugin.LogVerbose("Could not trim the camera culling mask: " + ex.Message); } } public static FaceTimeFeed TryCreate(FaceTimeConfig config, MonoBehaviour coroutineHost, Vector2Int resolution, float framerate) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) if (!OpenBodyCamsBridge.IsReady()) { Plugin.LogVerbose("OpenBodyCams is not initialised yet; deferring camera creation."); return null; } FaceTimeFeed faceTimeFeed = new FaceTimeFeed(config, coroutineHost, resolution, framerate); GameObject val = null; try { val = new GameObject("LethalFaceTime.PhoneCamera"); BodyCamComponent val2 = BodyCam.CreateBodyCam(val, (Renderer)null, -1, (ManualCameraRenderer)null); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)"OpenBodyCams returned no body cam component."); Object.Destroy((Object)(object)val); return null; } if (Array.IndexOf(BodyCamComponent.GetAllBodyCams(), val2) < 0) { Plugin.Log.LogWarning((object)"OpenBodyCams rejected the phone camera - its per-game setup has not finished. Will retry."); Object.Destroy((Object)(object)val); return null; } faceTimeFeed._host = val; faceTimeFeed._bodyCam = val2; val2.OnRenderTextureCreated += faceTimeFeed.HandleRenderTextureCreated; faceTimeFeed.CaptureCurrentTexture(); val2.ForceEnableCamera = true; val2.Framerate = framerate; if (val2.Resolution != resolution) { val2.Resolution = resolution; } faceTimeFeed.ApplyNightVision(); faceTimeFeed.ApplyFieldOfView(); Plugin.LogVerbose("Created a camera at " + ((Vector2Int)(ref resolution)).x + "x" + ((Vector2Int)(ref resolution)).y + " @ " + framerate + "fps."); return faceTimeFeed; } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to create the OpenBodyCams feed: " + ex)); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } faceTimeFeed._host = null; faceTimeFeed._bodyCam = null; return null; } } public bool SetTargetToPlayer(PlayerControllerB player, string description) { if (IsDead || (Object)(object)player == (Object)null) { return false; } return Retarget(delegate { _bodyCam.SetTargetToPlayer(player); }, description); } public bool SetTargetToTransform(Transform transform, string description) { if (IsDead || (Object)(object)transform == (Object)null) { return false; } return Retarget(delegate { _bodyCam.SetTargetToTransform(transform); }, description); } private bool Retarget(Action apply, string description) { try { apply(); _bodyCam.StartTargetTransition(); Plugin.LogVerbose("Phone camera now targeting " + description + "."); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to target the phone camera: " + ex)); return false; } } public void SetRendering(bool rendering) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (rendering && !IsDead) { try { Camera camera = _bodyCam.GetCamera(); if ((Object)(object)camera != (Object)null) { FacilityVisibility.NoteCamera(((Component)camera).transform.position); } } catch (Exception) { } } if (IsDead || _rendering == rendering) { return; } _rendering = rendering; try { ((Behaviour)_bodyCam).enabled = rendering; Plugin.LogVerbose(rendering ? "Phone camera resumed." : "Phone camera paused."); } catch (Exception ex2) { Plugin.Log.LogError((object)("Failed to toggle the phone camera: " + ex2)); } } public void ClearTarget() { if (IsDead) { return; } try { _bodyCam.SetTargetToNone(); } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to clear the phone camera target: " + ex)); } } public void ApplySettings(Vector2Int resolution, float framerate) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) if (IsDead) { return; } _resolution = resolution; _framerate = framerate; try { _bodyCam.Framerate = framerate; if (_bodyCam.Resolution != resolution) { _bodyCam.Resolution = resolution; } CaptureCurrentTexture(); ApplyNightVision(); ApplyFieldOfView(); } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to apply camera settings: " + ex)); } } public void ApplyFieldOfView() { if (IsDead) { return; } float value = _config.SelfieFieldOfView.Value; if (value <= 1f) { return; } try { Camera camera = _bodyCam.GetCamera(); if ((Object)(object)camera != (Object)null && !Mathf.Approximately(camera.fieldOfView, value)) { camera.fieldOfView = value; } } catch (Exception ex) { Plugin.LogVerbose("Could not set the camera field of view: " + ex.Message); } } public void ApplyNightVision() { if (IsDead || NightVisionLightField == null) { return; } try { object? value = NightVisionLightField.GetValue(_bodyCam); Light val = (Light)((value is Light) ? value : null); if (val != null) { float num = Mathf.Clamp01(_config.NightVisionStrength.Value); if (_baseNightVisionIntensity < 0f) { _baseNightVisionIntensity = val.intensity; } val.intensity = _baseNightVisionIntensity * num; } } catch (Exception ex) { Plugin.LogVerbose("Could not adjust the night-vision light: " + ex.Message); } } private void CaptureCurrentTexture() { if (IsDead) { return; } try { Camera camera = _bodyCam.GetCamera(); if ((Object)(object)camera != (Object)null && (Object)(object)camera.targetTexture != (Object)null) { HandleRenderTextureCreated(camera.targetTexture); } } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to read the camera's render texture: " + ex)); } } private void HandleRenderTextureCreated(RenderTexture texture) { if (!((Object)(object)texture == (Object)(object)_texture)) { RenderTexture texture2 = _texture; _texture = texture; try { this.TextureChanged?.Invoke(texture); } catch (Exception ex) { Plugin.Log.LogError((object)("A texture consumer threw: " + ex)); } ReleaseSupersededTexture(texture2, texture); } } private void ReleaseSupersededTexture(RenderTexture previous, RenderTexture current) { if (!_config.ReleaseOrphanedRenderTextures.Value || (Object)(object)previous == (Object)null || (Object)(object)previous == (Object)(object)current) { return; } try { Camera val = (IsDead ? null : _bodyCam.GetCamera()); if ((Object)(object)val != (Object)null && (Object)(object)val.targetTexture == (Object)(object)previous) { return; } } catch (Exception) { return; } ReleaseDeferred(previous); } private void ReleaseDeferred(RenderTexture texture) { if (!((Object)(object)texture == (Object)null)) { if ((Object)(object)_coroutineHost == (Object)null || !((Component)_coroutineHost).gameObject.activeInHierarchy) { DestroyTexture(texture); } else { _coroutineHost.StartCoroutine(ReleaseAfterFrames(texture, 2)); } } } private static IEnumerator ReleaseAfterFrames(RenderTexture texture, int frames) { for (int i = 0; i < frames; i++) { yield return null; } DestroyTexture(texture); } private void DestroyOwnedMonitorMaterial() { if ((Object)(object)_bodyCam == (Object)null || MonitorOnMaterialField == null) { return; } try { object? value = MonitorOnMaterialField.GetValue(_bodyCam); Material val = (Material)((value is Material) ? value : null); if ((Object)(object)val != (Object)null && ((Object)val).name == "BodyCamMaterial") { Object.Destroy((Object)(object)val); } } catch (Exception ex) { Plugin.LogVerbose("Could not free the body cam's generated material: " + ex.Message); } } private static void DestroyTexture(RenderTexture texture) { if ((Object)(object)texture == (Object)null) { return; } try { Object.Destroy((Object)(object)texture); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to free an orphaned render texture: " + ex.Message)); } } public void Dispose() { RenderTexture texture = _texture; _texture = null; if ((Object)(object)_bodyCam != (Object)null) { try { _bodyCam.OnRenderTextureCreated -= HandleRenderTextureCreated; _bodyCam.ForceEnableCamera = false; ((Behaviour)_bodyCam).enabled = false; _bodyCam.SetTargetToNone(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Error while detaching from the phone camera: " + ex.Message)); } } _rendering = false; DestroyOwnedMonitorMaterial(); if ((Object)(object)_host != (Object)null) { try { Object.Destroy((Object)(object)_host); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Error while destroying the phone camera host: " + ex2.Message)); } } _host = null; _bodyCam = null; if (_config.ReleaseOrphanedRenderTextures.Value) { ReleaseDeferred(texture); } Plugin.LogVerbose("Phone camera destroyed."); } } internal sealed class FaceTimeManager { private const int NoTargetKey = 0; private static FaceTimeManager _instance; private FaceTimeConfig _config; private PhoneScreen _screen; private FaceTimeFeed _feed; private PlayerPhone _trackedPhone; private ulong _sessionIdentity; private int _targetKey; private bool _inCall; private float _reconcileTimer; private float _idleTimer; private bool _idleTeardownDone; private bool _settingsDirty; private string _lastTraceKey; private bool _loggedFirstTick; private CameraFacing _facing; private readonly PhoneCameraRig _rig; private readonly SwitchboardMonitor _switchboardMonitor; private readonly OutgoingPreview _preview; private ScreenCorner _selfCorner; private bool _selfCornerInitialised; private readonly FacingSync _facingSync = new FacingSync(); private readonly RemotePhoneScreens _remoteScreens; private CallTarget _currentTarget; private int _infoTraceBudget = 6; private readonly HashSet<string> _warnedKeys = new HashSet<string>(); private readonly MonoBehaviour _host; internal static FaceTimeManager Instance => _instance; internal SwitchboardMonitor SwitchboardMonitor => _switchboardMonitor; public CameraFacing SelfFacing { get; private set; } public ScreenCorner SelfCorner { get { if (!_selfCornerInitialised) { _selfCorner = _config.PreviewCorner.Value; _selfCornerInitialised = true; } return _selfCorner; } } private FaceTimeManager(FaceTimeConfig config, MonoBehaviour host) { _config = config; _host = host; _screen = new PhoneScreen(config); _rig = new PhoneCameraRig(config); _switchboardMonitor = new SwitchboardMonitor(config, host); _preview = new OutgoingPreview(config, host, "LethalFaceTime.PhonePreview", posesLocalPlayer: true); _remoteScreens = new RemotePhoneScreens(config, host, _facingSync); _facing = config.DefaultFacing.Value; } public static void Create(FaceTimeConfig config, MonoBehaviour host) { if (_instance == null) { _instance = new FaceTimeManager(config, host); Plugin.Log.LogInfo((object)"Manager created; tick is driven by the plugin object."); } } public static void NotifyPhoneStateChanged(PlayerPhone phone) { if (_instance != null && !((Object)(object)phone == (Object)null) && !((Object)(object)phone != (Object)(object)LethalPhonesBridge.GetLocalPhone())) { _instance.Reconcile(); } } public static void NotifySettingsChanged() { if (_instance != null) { _instance._settingsDirty = true; } } public void Shutdown() { EndCall("the manager is shutting down"); DestroyFeed(); _screen?.Detach(); _rig?.Dispose(); _preview?.Dispose(); _remoteScreens?.Dispose(); FacilityVisibility.Restore(); _switchboardMonitor?.Dispose(); _facingSync.Shutdown(); LethalPhonesBridge.ForgetPhones(); MimicVoiceProximity.Forget(); if (_instance == this) { _instance = null; } } public void Tick() { if (_config == null) { return; } if (!_loggedFirstTick) { _loggedFirstTick = true; Plugin.Log.LogInfo((object)("Manager ticking. Enabled=" + _config.Enabled.Value + " Verbose=" + _config.VerboseLogging.Value + " ReconcileInterval=" + _config.ReconcileInterval.Value + "s Teardown=" + _config.TeardownBehaviour.Value)); } if (!_config.Enabled.Value) { if (_inCall || _feed != null || _screen.IsAttached) { EndCall("the mod was disabled"); DestroyFeed(); _screen.Detach(); } return; } if (_settingsDirty) { _settingsDirty = false; ApplyChangedSettings(); } _reconcileTimer += Time.deltaTime; if (_reconcileTimer >= _config.ReconcileInterval.Value) { _reconcileTimer = 0f; Reconcile(); } if (_inCall) { PollKeybinds(); TickActiveCall(); } else { TickIdle(); } _facingSync.Tick(SelfFacing, SelfCorner, Time.deltaTime); _switchboardMonitor.Tick(); _remoteScreens.Tick(Time.deltaTime); FacilityVisibility.Commit(_config.RenderFacilityDuringCalls.Value); } private void PollKeybinds() { FaceTimeInput keybinds = Plugin.Keybinds; if (keybinds == null) { return; } PlayerControllerB val = StartOfRound.Instance?.localPlayerController; if ((!((Object)(object)val != (Object)null) || (!val.inTerminalMenu && !val.isTypingChat)) && LethalPhonesBridge.IsPhoneOpen(_trackedPhone)) { if (keybinds.FlipCamera != null && keybinds.FlipCamera.WasPressedThisFrame()) { FlipCamera(); } if (keybinds.CyclePreviewCorner != null && keybinds.CyclePreviewCorner.WasPressedThisFrame()) { CyclePreviewCorner(1); } } } private void ApplyChangedSettings() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) _screen.ApplyLayout(); _feed?.ApplySettings(_config.Resolution, _config.Framerate.Value); if (_inCall && _feed != null && !_feed.IsDead) { _screen.SetTexture((Texture)(object)_feed.Texture); } if (!_inCall) { _idleTeardownDone = false; if (_config.TeardownBehaviour.Value == TeardownMode.DestroyImmediately) { DestroyFeed(); } } } private void Reconcile() { if (_config == null || !_config.Enabled.Value) { return; } PlayerPhone localPhone = LethalPhonesBridge.GetLocalPhone(); if ((Object)(object)localPhone == (Object)null) { Trace("waiting-for-phone", "No local phone yet (LethalPhones has not assigned one)."); EndCall("the local phone is gone"); return; } CallSnapshot call = LethalPhonesBridge.ReadCall((PhoneBehavior)(object)localPhone); if (!call.IsConnected) { Trace("idle", "Local phone found, no active call."); EndCall("the call ended"); return; } CallTarget target = LethalPhonesBridge.ResolveTarget(call.RemotePhone); if (target.IsValid && !target.IsPlayer && !_config.ShowVideoForNonPlayerCallers.Value) { EndCall("the far end is not a player and non-player video is disabled"); return; } Trace("connected", "Active call to " + target.Describe() + " (number " + call.RemoteNumber + ")."); if (!_inCall || _sessionIdentity != call.Identity || (Object)(object)_trackedPhone != (Object)(object)localPhone) { BeginCall(localPhone, call, target); } else { TryBringUpVideo(target); } } private void BeginCall(PlayerPhone phone, CallSnapshot call, CallTarget target) { if (_inCall) { RaiseCallEnded(); } _trackedPhone = phone; _sessionIdentity = call.Identity; _targetKey = 0; _inCall = true; _idleTimer = 0f; _idleTeardownDone = false; _facing = _config.DefaultFacing.Value; _rig.InvalidateMeasurement(); Plugin.Log.LogInfo((object)("FaceTime call connected to " + target.Describe() + ".")); FaceTimeEvents.RaiseCallConnected(new CallConnectedInfo(phone, call.RemotePhone, call.RemoteNumber, target.IsPlayer)); TryBringUpVideo(target); } private bool TryBringUpVideo(CallTarget target) { if (!_inCall || (Object)(object)_trackedPhone == (Object)null) { return false; } if (!target.IsValid) { Trace("unresolved-target", "Call connected but the far end could not be resolved yet."); return false; } if (_config.RequireBodyCamShipUpgrade.Value && !OpenBodyCamsBridge.BodyCamsAreUnlocked()) { WarnOnce("bodycam-locked", "The phone video feed is waiting on the ship's Bodycam upgrade, because Compatibility/RequireBodyCamShipUpgrade is enabled. Buy the upgrade, or set that option to false to decouple the phone from it."); return false; } if (!_screen.IsAttached && !_screen.TryAttach(_trackedPhone)) { Trace("no-screen", "Phone screen canvas not available yet; will retry."); return false; } if (!EnsureFeed()) { Trace("no-feed", "Could not create the OpenBodyCams camera yet; will retry."); return false; } if (!AimCamera(target)) { return false; } if (_config.HideFirstPersonPhoneFromCalls.Value) { int num = OpenBodyCamsBridge.ResolveOwnerOnlyLayer(_feed.CameraCullingMask); if (num >= 0) { _screen.HideHandsetFromCalls(num); } else { WarnOnce("owner-only-layer", "Found no layer that your own camera draws but body cams skip, so your held phone may appear in your outgoing picture. Set Camera/MountCameraOnPhone = false if that bothers you more than the wider framing helps."); } } _feed.SetRendering(ShouldRender()); _screen.SetTexture((Texture)(object)_feed.Texture); _screen.WarnIfVisibleToBodyCam(_feed.CameraCullingMask); Trace("video-up", "Video layer live. facing=" + _facing.ToString() + " texture=" + (((Object)(object)_feed.Texture != (Object)null) ? (((Texture)_feed.Texture).width + "x" + ((Texture)_feed.Texture).height) : "<null>") + " signal=" + _feed.HasSignal + " rendering=" + ShouldRender()); return true; } private bool AimCamera(CallTarget target) { _currentTarget = target; if (target.IsPlayer) { _facing = _facingSync.GetFacing(target.Player); } int num = ComputeTargetKey(target, _facing); bool flag = _config.MountCameraOnPhone.Value && (Object)(object)target.PhoneModel != (Object)null; if (_facing == CameraFacing.Front || !target.IsPlayer || flag) { _rig.RearFacing = _facing == CameraFacing.Rear; if (!_rig.Aim(target, _feed.Lens)) { Trace("no-anchor", "Could not frame a " + _facing.ToString() + " shot for " + target.Describe() + " yet."); return false; } if (num == _targetKey) { return true; } if (!_feed.SetTargetToTransform(_rig.Anchor, _facing.ToString() + " camera on " + target.Describe())) { return false; } _targetKey = num; return true; } if (num == _targetKey) { return true; } if (target.IsPlayer) { if (!_feed.SetTargetToPlayer(target.Player, "rear camera on " + target.Describe())) { return false; } _targetKey = num; return true; } Trace("unaimed", "No aiming strategy for " + target.Describe() + "."); return false; } public void FlipCamera() { SelfFacing = ((SelfFacing == CameraFacing.Front) ? CameraFacing.Rear : CameraFacing.Front); Plugin.Log.LogInfo((object)("Your outgoing camera flipped to " + SelfFacing.ToString() + "; announcing it to the other players.")); } public void CyclePreviewCorner(int step) { int num = ((step >= 0) ? 1 : (-1)); _selfCorner = (ScreenCorner)((int)(SelfCorner + num + 4) % 4); _selfCornerInitialised = true; Plugin.Log.LogInfo((object)("Your phone preview moved to " + _selfCorner.ToString() + ".")); } private void TickActiveCall() { if ((Object)(object)_trackedPhone == (Object)null) { EndCall("the tracked phone was destroyed"); } else if (_feed != null && !_feed.IsDead && _screen.IsAttached) { _feed.SetRendering(ShouldRender()); bool hasSignal = _feed.HasSignal; bool flag = LethalPhonesBridge.IsPhoneOpen(_trackedPhone); float interference = LethalPhonesBridge.GetInterference((PhoneBehavior)(object)_trackedPhone); _screen.UpdateAppearance(hasSignal, flag, interference); if (hasSignal && flag) { CallTarget target = LethalPhonesBridge.ResolveTarget((PhoneBehavior)(object)_trackedPhone); _preview.Draw(_screen.Canvas, target, SelfFacing, SelfCorner, interference); } else { _preview.Hide(); } } } public void LateTick() { if (_config != null && _config.Enabled.Value) { _switchboardMonitor.LateTick(); _preview.LateTick(); _remoteScreens.LateTick(); if (_inCall && _currentTarget.IsValid && (_facing != CameraFacing.Rear || !_currentTarget.IsPlayer) && _feed != null && !_feed.IsDead) { _rig.Aim(_currentTarget, _feed.Lens); } } } private void TickIdle() { if (_feed != null && !_idleTeardownDone && _config.TeardownBehaviour.Value == TeardownMode.DisableThenDestroyWhenIdle) { _idleTimer += Time.deltaTime; if (!(_idleTimer < _config.IdleKeepAliveSeconds.Value)) { _idleTeardownDone = true; Plugin.LogVerbose("Idle keep-alive elapsed; destroying the phone camera."); DestroyFeed(); } } } private bool ShouldRender() { if (_config.PauseFeedWhenPhoneStowed.Value) { return LethalPhonesBridge.IsPhoneOpen(_trackedPhone); } return true; } private bool EnsureFeed() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) if (_feed != null && !_feed.IsDead) { return true; } if (_feed != null) { DestroyFeed(); } _feed = FaceTimeFeed.TryCreate(_config, _host, _config.Resolution, _config.Framerate.Value); if (_feed == null) { return false; } _feed.TextureChanged += HandleFeedTextureChanged; _targetKey = 0; return true; } private void HandleFeedTextureChanged(RenderTexture texture) { if (_inCall) { _screen.SetTexture((Texture)(object)texture); } FaceTimeEvents.RaiseVideoTextureChanged(texture); } private void EndCall(string reason) { if (!_inCall) { return; } Plugin.Log.LogInfo((object)("FaceTime call disconnected because " + reason + ".")); RaiseCallEnded(); _inCall = false; _sessionIdentity = 0uL; _targetKey = 0; _idleTimer = 0f; _idleTeardownDone = false; _trackedPhone = null; _currentTarget = CallTarget.None; _screen.SetWidgetsHidden(hidden: false); _screen.SetTexture(null); if (_config.TeardownBehaviour.Value == TeardownMode.DestroyImmediately) { _preview.Dispose(); } else { _preview.Hide(); } switch (_config.TeardownBehaviour.Value) { case TeardownMode.DestroyImmediately: DestroyFeed(); break; case TeardownMode.DisableThenDestroyWhenIdle: case TeardownMode.DisableOnly: if (_feed != null && !_feed.IsDead) { _feed.SetRendering(rendering: false); _feed.ClearTarget(); _targetKey = 0; } break; } } private void RaiseCallEnded() { FaceTimeEvents.RaiseCallEnded(_trackedPhone); } private void DestroyFeed() { if (_feed != null) { _feed.TextureChanged -= HandleFeedTextureChanged; _feed.Dispose(); _feed = null; _targetKey = 0; _screen.SetTexture(null); FaceTimeEvents.RaiseVideoTextureChanged(null); } } private void Trace(string stateKey, string message) { if (!(_lastTraceKey == stateKey)) { _lastTraceKey = stateKey; string text = "[" + stateKey + "] " + message; if (_infoTraceBudget > 0) { _infoTraceBudget--; Plugin.Log.LogInfo((object)text); } else { Plugin.LogVerbose(text); } } } private void WarnOnce(string key, string message) { if (!_warnedKeys.Contains(key)) { _warnedKeys.Add(key); Plugin.Log.LogWarning((object)message); } } private static int ComputeTargetKey(CallTarget target, CameraFacing facing) { if (!target.IsValid) { return 0; } int num = ((target.IsPlayer && (Object)(object)target.Player != (Object)null) ? ((Object)target.Player).GetInstanceID() : (((Object)(object)target.Root != (Object)null) ? ((Object)target.Root).GetInstanceID() : 0)); if (num == 0) { return 0; } return num ^ ((int)(facing + 1) * 1597463007); } } internal static class FacilityVisibility { private const float InteriorY = -80f; private static bool _neededThisFrame; private static bool _suspended; private static bool _warned; public static void NoteCamera(Vector3 cameraPosition) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (cameraPosition.y < -80f) { _neededThisFrame = true; } } public static void Commit(bool allowed) { bool flag = allowed && _neededThisFrame; _neededThisFrame = false; if (flag == _suspended) { return; } MonoBehaviour val = ResolveCuller(); if ((Object)(object)val == (Object)null) { _suspended = false; return; } try { ((Behaviour)val).enabled = !flag; _suspended = flag; Plugin.LogVerbose(flag ? "Suspended the interior occlusion culler so a call can show the facility." : "Restored the interior occlusion culler."); } catch (Exception ex) { if (!_warned) { _warned = true; Plugin.Log.LogWarning((object)("Could not change the interior occlusion culler, so the facility may not render on the phone: " + ex.Message)); } } } public static void Restore() { _neededThisFrame = false; if (!_suspended) { return; } _suspended = false; try { MonoBehaviour val = ResolveCuller(); if ((Object)(object)val != (Object)null) { ((Behaviour)val).enabled = true; } } catch (Exception ex) { Plugin.LogVerbose("Could not restore the occlusion culler: " + ex.Message); } } private static MonoBehaviour ResolveCuller() { StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return (MonoBehaviour)(object)instance.occlusionCuller; } } internal sealed class FacingSync { private const string MessageName = "LethalFaceTime.Facing"; private const float ResendInterval = 5f; private readonly Dictionary<ulong, CameraFacing> _facings = new Dictionary<ulong, CameraFacing>(); private readonly Dictionary<ulong, ScreenCorner> _corners = new Dictionary<ulong, ScreenCorner>(); private NetworkManager _registeredWith; private CameraFacing _lastSent; private ScreenCorner _lastSentCorner; private bool _everSent; private float _resendTimer; private bool _warnedSendFailed; public void Tick(CameraFacing selfFacing, ScreenCorner selfCorner, float deltaTime) { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsListening) { _registeredWith = null; _everSent = false; _facings.Clear(); _corners.Clear(); return; } EnsureRegistered(singleton); _resendTimer += deltaTime; if (!_everSent || selfFacing != _lastSent || selfCorner != _lastSentCorner || !(_resendTimer < 5f)) { _resendTimer = 0f; _lastSent = selfFacing; _lastSentCorner = selfCorner; _everSent = true; Announce(singleton, selfFacing, selfCorner); } } public ScreenCorner GetCorner(PlayerControllerB player) { if ((Object)(object)player == (Object)null) { return ScreenCorner.BottomRight; } if (!_corners.TryGetValue(player.actualClientId, out var value)) { return ScreenCorner.BottomRight; } return value; } public CameraFacing GetFacing(PlayerControllerB player) { if ((Object)(object)player == (Object)null) { return CameraFacing.Front; } if (!_facings.TryGetValue(player.actualClientId, out var value)) { return CameraFacing.Front; } return value; } private void EnsureRegistered(NetworkManager manager) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown if ((Object)(object)_registeredWith == (Object)(object)manager) { return; } try { CustomMessagingManager customMessagingManager = manager.CustomMessagingManager; if (customMessagingManager != null) { customMessagingManager.RegisterNamedMessageHandler("LethalFaceTime.Facing", new HandleNamedMessageDelegate(HandleMessage)); _registeredWith = manager; Plugin.LogVerbose("Registered the camera-facing message handler."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not register the camera-facing handler: " + ex.Message)); } } private unsafe void Announce(NetworkManager manager, CameraFacing facing, ScreenCorner corner) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) try { if (manager.IsServer) { _facings[manager.LocalClientId] = facing; _corners[manager.LocalClientId] = corner; Broadcast(manager, manager.LocalClientId, facing, corner); return; } FastBufferWriter val = Build(manager.LocalClientId, facing, corner); try { manager.CustomMessagingManager.SendNamedMessage("LethalFaceTime.Facing", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { if (!_warnedSendFailed) { _warnedSendFailed = true; Plugin.Log.LogWarning((object)("Could not send your camera facing to the other players, so flipping will only change what the switchboard shows: " + ex.Message)); } } } private unsafe void Broadcast(NetworkManager manager, ulong subject, CameraFacing facing, ScreenCorner corner) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) FastBufferWriter val = Build(subject, facing, corner); try { manager.CustomMessagingManager.SendNamedMessageToAll("LethalFaceTime.Facing", val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static FastBufferWriter Build(ulong subject, CameraFacing facing, ScreenCorner corner) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) FastBufferWriter result = default(FastBufferWriter); ((FastBufferWriter)(ref result))..ctor(10, (Allocator)2, -1); ((FastBufferWriter)(ref result)).WriteValueSafe<ulong>(ref subject, default(ForPrimitives)); byte b = (byte)facing; ((FastBufferWriter)(ref result)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); b = (byte)corner; ((FastBufferWriter)(ref result)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); return result; } private void HandleMessage(ulong senderClientId, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) try { ulong key = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref key, default(ForPrimitives)); byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); CameraFacing cameraFacing = ((b == 1) ? CameraFacing.Rear : CameraFacing.Front); ScreenCorner screenCorner = ScreenCorner.BottomRight; if (((FastBufferReader)(ref reader)).Length - ((FastBufferReader)(ref reader)).Position >= 1) { byte b2 = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b2, default(ForPrimitives)); screenCorner = (ScreenCorner)(b2 & 3); } NetworkManager singleton = NetworkManager.Singleton; if (!((Object)(object)singleton == (Object)null)) { if (singleton.IsServer && senderClientId != singleton.LocalClientId) { _facings[senderClientId] = cameraFacing; _corners[senderClientId] = screenCorner; Broadcast(singleton, senderClientId, cameraFacing, screenCorner); } else { _facings[key] = cameraFacing; _corners[key] = screenCorner; } } } catch (Exception ex) { Plugin.LogVerbose("Malformed camera-facing message ignored: " + ex.Message); } } public void Shutdown() { if ((Object)(object)_registeredWith != (Object)null) { try { CustomMessagingManager customMessagingManager = _registeredWith.CustomMessagingManager; if (customMessagingManager != null) { customMessagingManager.UnregisterNamedMessageHandler("LethalFaceTime.Facing"); } } catch (Exception ex) { Plugin.LogVerbose("Error while unregistering the camera-facing handler: " + ex.Message); } } _registeredWith = null; _facings.Clear(); _corners.Clear(); _everSent = false; } } internal readonly struct Lens { private const float FallbackVerticalFov = 65f; private const float FallbackNearClip = 0.01f; private const float FallbackAspect = 1.3333334f; public readonly float TanHalfVertical; public readonly float TanHalfHorizontal; public readonly float NearClip; public Lens(Camera camera) { float num = 65f; float num2 = 1.3333334f; float nearClip = 0.01f; if ((Object)(object)camera != (Object)null) { if (camera.fieldOfView > 1f) { num = camera.fieldOfView; } if (camera.aspect > 0.01f) { num2 = camera.aspect; } if (camera.nearClipPlane > 0f) { nearClip = camera.nearClipPlane; } } TanHalfVertical = Mathf.Tan(num * 0.5f * (MathF.PI / 180f)); TanHalfHorizontal = TanHalfVertical * num2; NearClip = nearClip; } public float DistanceToFit(Vector3 size, float fill) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) fill = Mathf.Clamp(fill, 0.05f, 1f); float num = Mathf.Max(size.y, 0.05f) * 0.5f; float num2 = Mathf.Max(Mathf.Max(size.x, size.z), 0.05f) * 0.5f; float num3 = num / (TanHalfVertical * fill); float num4 = num2 / (TanHalfHorizontal * fill); return Mathf.Max(num3, num4); } } internal sealed class LocalPlayerPose { private static readonly FieldInfo ServerLeftArmRigField = AccessTools.Field(typeof(PlayerPhone), "serverLeftArmRig"); private static int _privateLayer = -1; private static readonly HashSet<LocalPlayerPose> Claims = new HashSet<LocalPlayerPose>(); private static PlayerPhone _posed; private static GameObject _model; private static int[] _originalLayers; private static Transform[] _modelTransforms; private static bool _layersApplied; private static bool _warnedNoRig; private static bool _warnedNoLayer; private static bool _loggedApplied; private FaceTimeFeed _feedShownTo; public void Apply(PlayerPhone localPhone, bool shouldPose, FaceTimeFeed feed, bool showPhoneToFeed = true) { if (!shouldPose || (Object)(object)localPhone == (Object)null) { Release(); return; } try { if ((Object)(object)_posed != (Object)null && (Object)(object)_posed != (Object)(object)localPhone) { ReleaseShared(); } if ((Object)(object)_posed == (Object)null) { _posed = localPhone; _model = localPhone.serverPhoneModel; _layersApplied = false; PlayFlip(localPhone, "PhoneFlipOpen"); } Claims.Add(this); if (!_layersApplied) { HideFromNormalCameras(); } if (showPhoneToFeed && _layersApplied && _privateLayer >= 0 && feed != null && _feedShownTo != feed) { feed.AddCullingLayer(_privateLayer); _feedShownTo = feed; } else if (!showPhoneToFeed) { _feedShownTo = null; } if (_layersApplied || _privateLayer == -2) { localPhone.SetPhoneServerModelActive(true); } SetArmWeight(localPhone, 1f); EnableRig(localPhone); if (!_loggedApplied) { _loggedApplied = true; Plugin.Log.LogInfo((object)("Posing the local player's third-person body to hold its phone" + ((_privateLayer >= 0) ? (" (on private layer " + _privateLayer + ")") : "") + ".")); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not pose the local player's phone hand: " + ex.Message)); } } public void Release() { _feedShownTo = null; if (Claims.Remove(this) && Claims.Count <= 0) { ReleaseShared(); } } private static void ReleaseShared() { Claims.Clear(); if ((Object)(object)_posed == (Object)null) { _model = null; _layersApplied = false; return; } try { RestoreLayers(); SetArmWeight(_posed, 0f); PlayFlip(_posed, "PhoneFlipClosed"); _posed.SetPhoneServerModelActive(false); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not restore the local player's phone hand: " + ex.Message)); } _posed = null; _model = null; _modelTransforms = null; _originalLayers = null; _layersApplied = false; } private static void HideFromNormalCameras() { if ((Object)(object)_model == (Object)null && (Object)(object)_posed != (Object)null) { _model = _posed.serverPhoneModel; } if ((Object)(object)_model == (Object)null) { return; } int num = ResolvePrivateLayer(); if (num < 0) { if (_privateLayer == -2 && !_warnedNoLayer) { _warnedNoLayer = true; Plugin.Log.LogWarning((object)"Found no spare Unity layer, so the third-person phone will be visible in your own view as well as in the call."); } return; } _modelTransforms = _model.GetComponentsInChildren<Transform>(true); _originalLayers = new int[_modelTransforms.Length]; for (int i = 0; i < _modelTransforms.Length; i++) { _originalLayers[i] = ((Component)_modelTransforms[i]).gameObject.layer; ((Component)_modelTransforms[i]).gameObject.layer = num; } _layersApplied = true; } private static void RestoreLayers() { if (_modelTransforms == null || _originalLayers == null) { return; } for (int i = 0; i < _modelTransforms.Length; i++) { if ((Object)(object)_modelTransforms[i] != (Object)null) { ((Component)_modelTransforms[i]).gameObject.layer = _originalLayers[i]; } } } private static int ResolvePrivateLayer() { if (_privateLayer != -1) { if (_privateLayer != -2) { return _privateLayer; } return -1; } PlayerControllerB val = StartOfRound.Instance?.localPlayerController; Camera val2 = (((Object)(object)val != (Object)null) ? val.gameplayCamera : null); if ((Object)(object)val2 == (Object)null) { return -1; } _privateLayer = -2; int cullingMask = val2.cullingMask; for (int num = 31; num >= 8; num--) { if ((cullingMask & (1 << num)) == 0 && string.IsNullOrEmpty(LayerMask.LayerToName(num))) { _privateLayer = num; break; } } if (_privateLayer != -2) { return _privateLayer; } return -1; } private static void EnableRig(PlayerPhone phone) { PlayerControllerB player = phone.player; if ((Object)(object)player == (Object)null || (Object)(object)player.meshContainer == (Object)null) { return; } Transform val = player.meshContainer.Find("metarig/Rig 1"); if (!((Object)(object)val == (Object)null)) { Rig component = ((Component)val).GetComponent<Rig>(); if ((Object)(object)component != (Object)null && component.weight < 1f) { component.weight = 1f; } Animator playerBodyAnimator = player.playerBodyAnimator; RigBuilder val2 = (((Object)(object)playerBodyAnimator != (Object)null) ? ((Component)playerBodyAnimator).GetComponent<RigBuilder>() : null); if ((Object)(object)val2 != (Object)null && !((Behaviour)val2).enabled) { ((Behaviour)val2).enabled = true; } } } private static void PlayFlip(PlayerPhone phone, string clip) { GameObject serverPhoneModel = phone.serverPhoneModel; if (!((Object)(object)serverPhoneModel == (Object)null)) { Animator component = serverPhoneModel.GetComponent<Animator>(); if ((Object)(object)component != (Object)null) { component.Play(clip); } } } private static void SetArmWeight(PlayerPhone phone, float weight) { if (ServerLeftArmRigField == null) { if (!_warnedNoRig) { _warnedNoRig = true; Plugin.Log.LogWarning((object)"Could not find PlayerPhone.serverLeftArmRig, so the local player's arm will not be posed and the phone may look like it is floating in your own feed."); } return; } object? value = ServerLeftArmRigField.GetValue(phone); ChainIKConstraint val = (ChainIKConstraint)((value is ChainIKConstraint) ? value : null); if (val != null) { ((RigConstraint<ChainIKConstraintJob, ChainIKConstraintData, ChainIKConstraintJobBinder<ChainIKConstraintData>>)(object)val).weight = weight; } } } internal static class MimicVoiceProximity { private const float Hysteresis = 2f; private const int MaxCachedSources = 512; private static readonly Dictionary<AudioSource, bool> IsMimic = new Dictionary<AudioSource, bool>(); private static readonly HashSet<AudioSource> Suppressed = new HashSet<AudioSource>(); private static Type _audioStreamType; private static FieldRef<object, AudioSource> _audioStreamSourceField; private static Func<object, AudioSource> _audioStreamSourceGetter; private static bool _resolved; private static bool _loggedMirage; private static bool _loggedFirstSuppression; public static void Consider(AudioSourceStorage storage) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) if (storage == null || (Object)(object)storage.recordPos == (Object)null) { return; } FaceTimeConfig settings = Plugin.Settings; if (settings == null || !settings.MimicVoiceKeepsProximity.Value) { return; } AudioSource audioSource = storage.audioSource; if ((Object)(object)audioSource == (Object)null || !IsMimicVoice(audioSource)) { return; } Vector3? val = ListenerPosition(); if (!val.HasValue) { return; } float num = Vector3.Distance(val.Value, ((Component)audioSource).transform.position); float value = settings.MimicVoiceProximityDistance.Value; if (Suppressed.Contains(audioSource)) { if (num > value + 2f) { Suppressed.Remove(audioSource); return; } } else { if (num > value) { return; } Suppressed.Add(audioSource); } storage.recordPos = null; storage.playPos = null; storage.listenerPos = null; if (!_loggedFirstSuppression) { _loggedFirstSuppression = true; Plugin.Log.LogInfo((object)("A Mirage mimic voice was close enough (" + num.ToString("0.0") + "m) to be heard where it is standing, so it was left out of the phone line. Set MimicVoiceKeepsProximity=false to restore LethalPhones' own behaviour.")); } } public static void Forget() { IsMimic.Clear(); Suppressed.Clear(); } private static bool IsMimicVoice(AudioSource source) { if (IsMimic.TryGetValue(source, out var value)) { return value; } bool flag = Resolve(source); if (IsMimic.Count >= 512) { IsMimic.Clear(); } IsMimic[source] = flag; return flag; } private static bool Resolve(AudioSource source) { if (!EnsureMirageResolved()) { return false; } try { if ((Object)(object)((Component)source).GetComponent(_audioStreamType) != (Object)null) { return true; } Component componentInParent = ((Component)source).GetComponentInParent(_audioStreamType); if ((Object)(object)componentInParent == (Object)null) { return false; } AudioSource val = ReadStreamSource(componentInParent); return (Object)(object)val != (Object)null && (Object)(object)val == (Object)(object)source; } catch (Exception ex) { Plugin.LogVerbose("Could not tell whether an audio source is a mimic voice: " + ex.Message); return false; } } private static AudioSource ReadStreamSource(object stream) { if (_audioStreamSourceField != null) { return _audioStreamSourceField.Invoke(stream); } return _audioStreamSourceGetter?.Invoke(stream); } private static bool EnsureMirageResolved() { if (_resolved) { return _audioStreamType != null; } _resolved = true; _audioStreamType = AccessTools.TypeByName("Mirage.Unity.AudioStream") ?? FindTypeByShortName("AudioStream"); if (_audioStreamType == null) { return false; } try { FieldInfo fieldInfo = AccessTools.Field(_audioStreamType, "AudioSource"); if (fieldInfo != null && fieldInfo.FieldType == typeof(AudioSource)) { _audioStreamSourceField = AccessTools.FieldRefAccess<object, AudioSource>(fieldInfo); } if (_audioStreamSourceField == null) { MethodInfo getter = AccessTools.PropertyGetter(_audioStreamType, "AudioSource"); if (getter != null && getter.ReturnType == typeof(AudioSource)) { _audioStreamSourceGetter = (object instance) => (AudioSource)getter.Invoke(instance, null); } } } catch (Exception ex) { Plugin.LogVerbose("Could not read Mirage's AudioStream.AudioSource (" + ex.Message + "); falling back to matching the audio source's own object."); } if (!_loggedMirage) { _loggedMirage = true; Plugin.Log.LogInfo((object)"Mirage found; mimic voices will keep playing from the creature while you are near it, instead of only through the phone."); } return true; } private static Type FindTypeByShortName(string name) { try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (assembly.FullName == null || !assembly.FullName.StartsWith("Mirage")) { continue; } Type[] types = assembly.GetTypes(); foreach (Type type in types) { if (type.Name == name && typeof(Component).IsAssignableFrom(type)) { return type; } } } } catch (Exception ex) { Plugin.LogVerbose("Could not scan for Mirage's " + name + ": " + ex.Message); } return null; } private static Vector3? ListenerPosition() { //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; PlayerControllerB val = (((Object)(object)instance != (Object)null) ? instance.localPlayerController : null); if ((Object)(object)val == (Object)null) { return null; } if (val.isPlayerDead && (Object)(object)val.spectatedPlayerScript != (Object)null && (Object)(object)val.spectatedPlayerScript.playerGlobalHead != (Object)null) { return val.spectatedPlayerScript.playerGlobalHead.position; } return ((Object)(object)val.playerGlobalHead != (Object)null) ? val.playerGlobalHead.position : ((Component)val).transform.position; } } internal sealed class NetworkedCorner { private const float ResendInterval = 5f; private readonly string _messageName; private NetworkManager _registeredWith; private float _resendTimer; private bool _warnedSendFailed; public ScreenCorner Value { get; private set; } public event Action<ScreenCorner> Changed; public NetworkedCorner(string messageName, ScreenCorner initial) { _messageName = messageName; Value = initial; } public void Tick(float deltaTime) { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsListening) { _registeredWith = null; return; } EnsureRegistered(singleton); if (singleton.IsServer) { _resendTimer += deltaTime; if (!(_resendTimer < 5f)) { _resendTimer = 0f; TryBroadcast(singleton, Value); } } } public void RequestCycle(int step) { int num = ((step >= 0) ? 1 : (-1)); Request((ScreenCorner)((int)(Value + num + 4) % 4)); } public unsafe void Request(ScreenCorner corner) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) Apply(corner); NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsListening) { return; } try { if (singleton.IsServer) { TryBroadcast(singleton, corner); return; } FastBufferWriter val = Build(corner); try { singleton.CustomMessagingManager.SendNamedMessage(_messageName, 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { if (!_warnedSendFailed) { _warnedSendFailed = true; Plugin.Log.LogWarning((object)("Could not share the switchboard preview corner with the other players, so it will only move on your screen: " + ex.Message)); } } } private void Apply(ScreenCorner corner) { if (Value == corner) { return; } Value = corner; try { this.Changed?.Invoke(corner); } catch (Exception ex) { Plugin.Log.LogError((object)("A shared-corner subscriber threw: " + ex)); } } private void EnsureRegistered(NetworkManager manager) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown if ((Object)(object)_registeredWith == (Object)(object)manager) { return; } try { CustomMessagingManager customMessagingManager = manager.CustomMessagingManager; if (customMessagingManager != null) { customMessagingManager.RegisterNamedMessageHandler(_messageName, new HandleNamedMessageDelegate(HandleMessage)); _registeredWith = manager; Plugin.LogVerbose("Registered the shared-corner handler for " + _messageName + "."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not register " + _messageName + ": " + ex.Message)); } } private unsafe void TryBroadcast(NetworkManager manager, ScreenCorner corner) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) try { FastBufferWriter val = Build(corner); try { manager.CustomMessagingManager.SendNamedMessageToAll(_messageName, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Plugin.LogVerbose("Could not broadcast " + _messageName + ": " + ex.Message); } } private static FastBufferWriter Build(ScreenCorner corner) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) FastBufferWriter result = default(FastBufferWriter); ((FastBufferWriter)(ref result))..ctor(1, (Allocator)2, -1); byte b = (byte)corner; ((FastBufferWriter)(ref result)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); return result; } private void HandleMessage(ulong senderClientId, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) try { byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); ScreenCorner corner = (ScreenCorner)(b & 3); NetworkManager singleton = NetworkManager.Singleton; if (!((Object)(object)singleton == (Object)null)) { Apply(corner); if (singleton.IsServer && senderClientId != singleton.LocalClientId) { TryBroadcast(singleton, corner); } } } catch (Exception ex) { Plugin.LogVerbose("Malformed " + _messageName + " message ignored: " + ex.Message); } } public void Shutdown() { if ((Object)(object)_registeredWith != (Object)null) { try { CustomMessagingManager customMessagingManager = _registeredWith.CustomMessagingManager; if (customMessagingManager != null) { customMessagingManager.UnregisterNamedMessageHandler(_messageName); } } catch (Exception ex) { Plugin.LogVerbose("Error unregistering " + _messageName + ": " + ex.Message); } } _registeredWith = null; } } internal sealed class NetworkedFlag { private const float ResendInterval = 5f; private readonly string _messageName; private NetworkManager _registeredWith; private float _resendTimer; private bool _warnedSendFailed; public bool Value { get; private set; } public event Action<bool> Changed; public NetworkedFlag(string messageName) { _messageName = messageName; } public void Tick(float deltaTime) { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsListening) { _registeredWith = null; return; } EnsureRegistered(singleton); if (singleton.IsServer) { _resendTimer += deltaTime; if (!(_resendTimer < 5f)) { _resendTimer = 0f; TryBroadcast(singleton, Value); } } } public unsafe void Request(bool value) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) Apply(value); NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsListening) { return; } try { if (singleton.IsServer) { TryBroadcast(singleton, value); return; } FastBufferWriter val = Build(value); try { singleton.CustomMessagingManager.SendNamedMessage(_messageName, 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { if (!_warnedSendFailed) { _warnedSendFailed = true; Plugin.Log.LogWarning((object)("Could not share the switchboard view with the other players, so it will only change on your screen: " + ex.Message)); } } } private void Apply(bool value) { if (Value == value) { return; } Value = value; try { this.Changed?.Invoke(value); } catch (Exception ex) { Plugin.Log.LogError((object)("A shared-flag subscriber threw: " + ex)); } } private void EnsureRegistered(NetworkManager manager) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown if ((Object)(object)_registeredWith == (Object)(object)manager) { return; } try { CustomMessagingManager customMessagingManager = manager.CustomMessagingManager; if (customMessagingManager != null) { customMessagingManager.RegisterNamedMessageHandler(_messageName, new HandleNamedMessageDelegate(HandleMessage)); _registeredWith = manager; Plugin.LogVerbose("Registered the shared-flag handler for " + _messageName + "."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not register " + _messageName + ": " + ex.Message)); } } private unsafe void TryBroadcast(NetworkManager manager, bool value) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) try { FastBufferWriter val = Build(value); try { manager.CustomMessagingManager.SendNamedMessageToAll(_messageName, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Plugin.LogVerbose("Could not broadcast " + _messageName + ": " + ex.Message); } } private static FastBufferWriter Build(bool value) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) FastBufferWriter result = default(FastBufferWriter); ((FastBufferWriter)(ref result))..ctor(1, (Allocator)2, -1); byte b = (value ? ((byte)1) : ((byte)0)); ((FastBufferWriter)(ref result)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); return result; } private void HandleMessage(ulong senderClientId, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (migh