Decompiled source of Craft PEAK v0.0.5
Craft PEAK.dll
Decompiled a week 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.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.SceneManagement; using UnityEngine.UI; using Zorro.Core; using Zorro.Core.Serizalization; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Craft PEAK")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Craft PEAK")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("1380deb8-d6dd-4eef-8491-f716b480ac63")] [assembly: AssemblyFileVersion("0.0.5.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.0.5.0")] namespace CraftPeak; [BepInPlugin("com.sappheiros.crafting.campfire", "Craft PEAK Campfire Materials", "0.0.5")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class CampfireGate : BaseUnityPlugin, IOnEventCallback { private sealed class IngredientLocation { public Player Player; public Character Character; public ItemSlot Slot; public bool IsBackpackInternal; public byte ExternalSlotId; public int BackpackSlotIndex; public ushort ItemId; public int AvailableCount; } private sealed class IngredientPlan { public readonly List<IngredientLocation> FireWood = new List<IngredientLocation>(); public readonly List<IngredientLocation> Stone = new List<IngredientLocation>(); public readonly List<IngredientLocation> Torch = new List<IngredientLocation>(); } private struct ConsumedSelectedSlot { public int ActorNumber; public int SlotId; } public const string PluginGuid = "com.sappheiros.crafting.campfire"; public const string PluginName = "Craft PEAK Campfire Materials"; public const string PluginVersion = "0.0.5"; public const ushort FireWoodItemId = 28; public const ushort StoneItemId = 72; public const ushort TorchItemId = 109; private const int DefaultRequiredFireWoodCount = 1; private const int DefaultRequiredStoneCount = 1; private const int DefaultRequiredTorchCount = 1; private const bool DefaultRequireEveryoneInRange = true; private const float DefaultEveryoneInRangeDistance = 15f; private const float DefaultMaximumRequesterDistance = 4f; private const int MaximumConfigMaterialCount = 20; private const float MinimumConfigDistance = 1f; private const float MaximumConfigDistance = 50f; private static readonly MethodInfo OriginalLightRpcMethod = AccessTools.Method(typeof(Campfire), "Light_Rpc", new Type[2] { typeof(bool), typeof(float) }, (Type[])null); private static ConfigEntry<int> requiredFireWoodCountConfig; private static ConfigEntry<int> requiredStoneCountConfig; private static ConfigEntry<int> requiredTorchCountConfig; private static ConfigEntry<bool> requireEveryoneInRangeConfig; private static ConfigEntry<float> everyoneInRangeDistanceConfig; private static ConfigEntry<float> maximumRequesterDistanceConfig; private const byte IgniteRequestEventCode = 170; private const byte IgniteResultEventCode = 171; private const byte ConsumedSelectedSlotEventCode = 172; private readonly HashSet<int> committedCampfireViewIds = new HashSet<int>(); public static int RequiredFireWoodCount { get; private set; } = 1; public static int RequiredStoneCount { get; private set; } = 1; public static int RequiredTorchCount { get; private set; } = 1; public static bool RequireEveryoneInRange { get; private set; } = true; public static float EveryoneInRangeDistance { get; private set; } = 15f; public static float MaximumRequesterDistance { get; private set; } = 4f; internal static CampfireGate Instance { get; private set; } internal static ManualLogSource ModLogger { get; private set; } internal static bool Enabled { get; private set; } private void Awake() { Instance = this; ModLogger = ((BaseUnityPlugin)this).Logger; BindCampfireConfig(); Enabled = true; SceneManager.sceneLoaded += HandleSceneLoaded; LogCurrentCampfireConditions("Loaded"); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Campfire Photon event codes=" + (byte)170 + "-" + (byte)172 + " (Photon-safe range)")); } private void BindCampfireConfig() { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Expected O, but got Unknown requiredFireWoodCountConfig = ((BaseUnityPlugin)this).Config.Bind<int>("01. 캠프파이어 재료 조건", "나뭇가지 요구 수량", 1, new ConfigDescription("모닥불 하나를 점화할 때 소비할 나뭇가지 수량입니다. 0으로 설정하면 나뭇가지를 요구하거나 소비하지 않습니다.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 20), Array.Empty<object>())); requiredStoneCountConfig = ((BaseUnityPlugin)this).Config.Bind<int>("01. 캠프파이어 재료 조건", "돌 요구 수량", 1, new ConfigDescription("모닥불 하나를 점화할 때 소비할 돌 수량입니다. 0으로 설정하면 돌을 요구하거나 소비하지 않습니다.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 20), Array.Empty<object>())); requiredTorchCountConfig = ((BaseUnityPlugin)this).Config.Bind<int>("01. 캠프파이어 재료 조건", "횃불 요구 수량", 1, new ConfigDescription("모닥불 하나를 점화할 때 소비할 횃불 수량입니다. 0으로 설정하면 횃불을 요구하거나 소비하지 않습니다.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 20), Array.Empty<object>())); requireEveryoneInRangeConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("02. 캠프파이어 집결 조건", "모든 생존 플레이어 집결 필요", true, "활성화하면 기존 PEAK처럼 모든 생존 플레이어가 모닥불 근처에 모여야 점화할 수 있습니다."); everyoneInRangeDistanceConfig = ((BaseUnityPlugin)this).Config.Bind<float>("02. 캠프파이어 집결 조건", "집결 판정 거리", 15f, new ConfigDescription("모든 생존 플레이어 집결 조건에 사용하는 모닥불 중심 거리입니다.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 50f), Array.Empty<object>())); maximumRequesterDistanceConfig = ((BaseUnityPlugin)this).Config.Bind<float>("02. 캠프파이어 집결 조건", "점화 요청 허용 거리", 4f, new ConfigDescription("상호작용한 플레이어가 호스트 검증 시 모닥불에서 떨어질 수 있는 최대 거리입니다.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 15f), Array.Empty<object>())); requiredFireWoodCountConfig.SettingChanged += HandleCampfireConfigChanged; requiredStoneCountConfig.SettingChanged += HandleCampfireConfigChanged; requiredTorchCountConfig.SettingChanged += HandleCampfireConfigChanged; requireEveryoneInRangeConfig.SettingChanged += HandleCampfireConfigChanged; everyoneInRangeDistanceConfig.SettingChanged += HandleCampfireConfigChanged; maximumRequesterDistanceConfig.SettingChanged += HandleCampfireConfigChanged; ApplyCampfireConfigValues(); } private static void HandleCampfireConfigChanged(object sender, EventArgs eventArgs) { ApplyCampfireConfigValues(); LogCurrentCampfireConditions("Config changed"); } private static void ApplyCampfireConfigValues() { RequiredFireWoodCount = ((requiredFireWoodCountConfig == null) ? 1 : Mathf.Clamp(requiredFireWoodCountConfig.Value, 0, 20)); RequiredStoneCount = ((requiredStoneCountConfig == null) ? 1 : Mathf.Clamp(requiredStoneCountConfig.Value, 0, 20)); RequiredTorchCount = ((requiredTorchCountConfig == null) ? 1 : Mathf.Clamp(requiredTorchCountConfig.Value, 0, 20)); RequireEveryoneInRange = requireEveryoneInRangeConfig == null || requireEveryoneInRangeConfig.Value; EveryoneInRangeDistance = ((everyoneInRangeDistanceConfig != null) ? Mathf.Clamp(everyoneInRangeDistanceConfig.Value, 1f, 50f) : 15f); MaximumRequesterDistance = ((maximumRequesterDistanceConfig != null) ? Mathf.Clamp(maximumRequesterDistanceConfig.Value, 1f, 15f) : 4f); } private static void LogCurrentCampfireConditions(string reason) { if (ModLogger != null) { ModLogger.LogInfo((object)("Craft PEAK Campfire Materials 0.0.5 conditions applied. Reason=" + reason + " | FireWood(" + (ushort)28 + ") x" + RequiredFireWoodCount + " | Stone(" + (ushort)72 + ") x" + RequiredStoneCount + " | Torch(" + (ushort)109 + ") x" + RequiredTorchCount + " | RequireEveryone=" + RequireEveryoneInRange + " | EveryoneRange=" + EveryoneInRangeDistance + " | RequesterRange=" + MaximumRequesterDistance + ". Host settings are authoritative.")); } } private void OnEnable() { PhotonNetwork.AddCallbackTarget((object)this); } private void OnDisable() { PhotonNetwork.RemoveCallbackTarget((object)this); } private void OnDestroy() { Enabled = false; SceneManager.sceneLoaded -= HandleSceneLoaded; UnbindCampfireConfigEvents(); committedCampfireViewIds.Clear(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } ModLogger = null; } private static void UnbindCampfireConfigEvents() { if (requiredFireWoodCountConfig != null) { requiredFireWoodCountConfig.SettingChanged -= HandleCampfireConfigChanged; } if (requiredStoneCountConfig != null) { requiredStoneCountConfig.SettingChanged -= HandleCampfireConfigChanged; } if (requiredTorchCountConfig != null) { requiredTorchCountConfig.SettingChanged -= HandleCampfireConfigChanged; } if (requireEveryoneInRangeConfig != null) { requireEveryoneInRangeConfig.SettingChanged -= HandleCampfireConfigChanged; } if (everyoneInRangeDistanceConfig != null) { everyoneInRangeDistanceConfig.SettingChanged -= HandleCampfireConfigChanged; } if (maximumRequesterDistanceConfig != null) { maximumRequesterDistanceConfig.SettingChanged -= HandleCampfireConfigChanged; } requiredFireWoodCountConfig = null; requiredStoneCountConfig = null; requiredTorchCountConfig = null; requireEveryoneInRangeConfig = null; everyoneInRangeDistanceConfig = null; maximumRequesterDistanceConfig = null; } private void HandleSceneLoaded(Scene scene, LoadSceneMode loadSceneMode) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) committedCampfireViewIds.Clear(); if (IsExcludedScene(scene)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Campfire material gate disabled in scene: " + ((Scene)(ref scene)).name)); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Campfire material gate enabled in scene: " + ((Scene)(ref scene)).name)); } } internal static bool IsGameplayActive() { //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (!Enabled) { return false; } if (IsExcludedScene(SceneManager.GetActiveScene())) { return false; } return (Object)(object)Object.FindAnyObjectByType<MapHandler>() != (Object)null; } internal static bool IsManagedCampfire(Campfire campfire) { if (!IsGameplayActive() || (Object)(object)campfire == (Object)null) { return false; } try { return (Object)(object)MapHandler.CurrentCampfire == (Object)(object)campfire; } catch (Exception) { return false; } } private static bool IsExcludedScene(Scene scene) { if (!((Scene)(ref scene)).IsValid() || !((Scene)(ref scene)).isLoaded) { return true; } if (!string.Equals(((Scene)(ref scene)).name, "Airport", StringComparison.OrdinalIgnoreCase) && !string.Equals(((Scene)(ref scene)).name, "Title", StringComparison.OrdinalIgnoreCase)) { return string.Equals(((Scene)(ref scene)).name, "Pretitle", StringComparison.OrdinalIgnoreCase); } return true; } internal void RequestIgnition(Campfire campfire, Character interactor) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Expected O, but got Unknown //IL_0156: Unknown result type (might be due to invalid IL or missing references) if (!IsManagedCampfire(campfire) || campfire.Lit || (int)campfire.state != 0) { return; } if (!PhotonNetwork.InRoom) { NotifyLocalPlayer("모닥불 점화 실패: 네트워크 방에 연결되어 있지 않습니다."); return; } if ((Object)(object)interactor == (Object)null || (Object)(object)((MonoBehaviourPun)interactor).photonView == (Object)null || !interactor.IsLocal) { NotifyLocalPlayer("모닥불 점화 실패: 상호작용 플레이어를 확인할 수 없습니다."); return; } PhotonView component = ((Component)campfire).GetComponent<PhotonView>(); if ((Object)(object)component == (Object)null || component.ViewID <= 0) { NotifyLocalPlayer("모닥불 점화 실패: 모닥불 네트워크 정보를 찾지 못했습니다."); return; } int num = ((((MonoBehaviourPun)interactor).photonView.Owner != null) ? ((MonoBehaviourPun)interactor).photonView.Owner.ActorNumber : (-1)); if (num <= 0) { NotifyLocalPlayer("모닥불 점화 실패: 플레이어 네트워크 번호를 찾지 못했습니다."); return; } object[] array = new object[1] { component.ViewID }; ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Request prepared. Actor=" + num + " | CampfireViewID=" + component.ViewID + " | IsMaster=" + PhotonNetwork.IsMasterClient + " | EventCode=" + (byte)170)); if (PhotonNetwork.IsMasterClient) { ProcessIgniteRequestOnHost(num, array); return; } RaiseEventOptions val = new RaiseEventOptions { Receivers = (ReceiverGroup)2 }; if (!PhotonNetwork.RaiseEvent((byte)170, (object)array, val, SendOptions.SendReliable)) { NotifyLocalPlayer("모닥불 점화 요청 전송에 실패했습니다."); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Campfire ignite request sent to host. Actor=" + num + " | CampfireViewID=" + component.ViewID)); } } public void OnEvent(EventData photonEvent) { if (photonEvent == null) { return; } if (photonEvent.Code == 170) { if (PhotonNetwork.IsMasterClient) { ProcessIgniteRequestOnHost(photonEvent.Sender, photonEvent.CustomData as object[]); } } else if (photonEvent.Code == 171) { HandleIgniteResult(photonEvent.CustomData as object[]); } else if (photonEvent.Code == 172) { HandleConsumedSelectedSlots(photonEvent.CustomData as object[]); } } private void ProcessIgniteRequestOnHost(int requesterActorNumber, object[] requestData) { //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_03cf: Unknown result type (might be due to invalid IL or missing references) //IL_03d4: Unknown result type (might be due to invalid IL or missing references) if (!PhotonNetwork.IsMasterClient) { return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Host received ignition request. Actor=" + requesterActorNumber + " | PayloadLength=" + ((requestData != null) ? requestData.Length : 0))); if (!IsGameplayActive()) { SendIgniteResult(requesterActorNumber, success: false, "현재 씬에서는 모닥불 재료 기능이 작동하지 않습니다."); return; } if (requestData == null || requestData.Length < 1) { SendIgniteResult(requesterActorNumber, success: false, "잘못된 모닥불 점화 요청입니다."); return; } int num; try { num = Convert.ToInt32(requestData[0]); } catch (Exception) { SendIgniteResult(requesterActorNumber, success: false, "모닥불 네트워크 정보를 해석하지 못했습니다."); return; } if (num <= 0) { SendIgniteResult(requesterActorNumber, success: false, "잘못된 모닥불 네트워크 번호입니다."); return; } if (committedCampfireViewIds.Contains(num)) { SendIgniteResult(requesterActorNumber, success: false, "이 모닥불은 이미 점화 처리 중이거나 켜져 있습니다."); return; } PhotonView val = PhotonView.Find(num); if ((Object)(object)val == (Object)null) { SendIgniteResult(requesterActorNumber, success: false, "모닥불을 찾을 수 없습니다."); return; } Campfire component = ((Component)val).GetComponent<Campfire>(); if (!IsManagedCampfire(component)) { SendIgniteResult(requesterActorNumber, success: false, "이 모닥불은 재료 조건 적용 대상이 아닙니다."); return; } if (component.Lit || (int)component.state != 0) { SendIgniteResult(requesterActorNumber, success: false, "이 모닥불은 이미 켜졌거나 사용할 수 없습니다."); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Campfire target validated. ViewID=" + num + " | Lit=" + component.Lit + " | State=" + ((object)component.state/*cast due to .constrained prefix*/).ToString())); Player player = PlayerHandler.GetPlayer(requesterActorNumber); Character val2 = (((Object)(object)player != (Object)null) ? player.character : null); if ((Object)(object)player == (Object)null || (Object)(object)val2 == (Object)null || (Object)(object)val2.data == (Object)null || val2.data.dead) { SendIgniteResult(requesterActorNumber, success: false, "점화를 요청한 플레이어를 확인할 수 없습니다."); return; } float num2 = Vector3.Distance(component.Center(), val2.Center); if (num2 > MaximumRequesterDistance) { SendIgniteResult(requesterActorNumber, success: false, "모닥불에서 너무 멀리 떨어져 있습니다."); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Requester distance validated. Actor=" + requesterActorNumber + " | Distance=" + num2.ToString("0.00") + " | Maximum=" + MaximumRequesterDistance.ToString("0.00"))); string value = default(string); if (RequireEveryoneInRange && !component.EveryoneInRange(ref value, EveryoneInRangeDistance)) { string message = (string.IsNullOrEmpty(value) ? "모든 생존 플레이어가 모닥불 근처에 모여야 합니다." : StripRichTextForNotification(value)); SendIgniteResult(requesterActorNumber, success: false, message); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Player range condition passed. RequireEveryone=" + RequireEveryoneInRange + " | EveryoneRange=" + EveryoneInRangeDistance.ToString("0.00"))); if (!TryCreateIngredientPlan(out var plan, out var missingMessage)) { SendIgniteResult(requesterActorNumber, success: false, missingMessage); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Campfire ignite denied: missing materials. Actor=" + requesterActorNumber + " | CampfireViewID=" + num + " | " + BuildMaterialCountLog())); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Material condition passed. " + BuildMaterialCountLog())); committedCampfireViewIds.Add(num); if (!TryConsumeIngredientPlan(plan, out var consumedSelectedSlots)) { committedCampfireViewIds.Remove(num); SendIgniteResult(requesterActorNumber, success: false, "재료 소비 중 오류가 발생했습니다. 재료는 소비되지 않았습니다."); return; } BroadcastConsumedSelectedSlots(consumedSelectedSlots); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] All conditions passed. Calling Light_Rpc(true, 0f). Actor=" + requesterActorNumber + " | CampfireViewID=" + num + " | BeforeLit=" + component.Lit + " | BeforeState=" + ((object)component.state/*cast due to .constrained prefix*/).ToString())); try { val.RPC("Light_Rpc", (RpcTarget)0, new object[2] { true, 0f }); } catch (Exception ex2) { committedCampfireViewIds.Remove(num); ((BaseUnityPlugin)this).Logger.LogError((object)("[CampfireIgnitionDiag] Photon Light_Rpc(true, 0f) threw an exception. CampfireViewID=" + num + " | Exception=" + ex2)); SendIgniteResult(requesterActorNumber, success: false, "모닥불 점화 RPC 호출 중 예외가 발생했습니다. 점화 잠금을 해제했습니다."); return; } ((MonoBehaviour)this).StartCoroutine(VerifyCorrectArgumentRpcResult(requesterActorNumber, num, component)); } private IEnumerator VerifyCorrectArgumentRpcResult(int requesterActorNumber, int campfireViewId, Campfire campfire) { yield return (object)new WaitForSecondsRealtime(0.25f); bool flag = IsCampfireActuallyLit(campfire); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[CampfireIgnitionDiag] Light_Rpc(true, 0f) verification. Actor=" + requesterActorNumber + " | CampfireViewID=" + campfireViewId + " | Lit=" + ((Object)(object)campfire != (Object)null && campfire.Lit) + " | State=" + (((Object)(object)campfire != (Object)null) ? ((object)campfire.state/*cast due to .constrained prefix*/).ToString() : "<destroyed>") + " | Success=" + flag)); if (flag) { SendIgniteResult(requesterActorNumber, success: true, "모닥불 점화 성공\n" + BuildConsumedMaterialMessage()); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Campfire ignition verified with correct RPC arguments. Actor=" + requesterActorNumber + " | CampfireViewID=" + campfireViewId + " | updateSegment=True | burningFor=0 | Consumed: FireWood=" + RequiredFireWoodCount + ", Stone=" + RequiredStoneCount + ", Torch=" + RequiredTorchCount + ".")); } else { committedCampfireViewIds.Remove(campfireViewId); SendIgniteResult(requesterActorNumber, success: false, "Light_Rpc(true, 0f)를 호출했지만 모닥불 상태가 변경되지 않았습니다. 점화 잠금을 해제했습니다."); ((BaseUnityPlugin)this).Logger.LogError((object)("[CampfireIgnitionDiag] Correct-argument Light_Rpc did not activate campfire. Actor=" + requesterActorNumber + " | CampfireViewID=" + campfireViewId + " | LockReleased=True")); } } private static bool IsCampfireActuallyLit(Campfire campfire) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 if ((Object)(object)campfire == (Object)null) { return false; } if (!campfire.Lit) { return (int)campfire.state > 0; } return true; } private static bool TryCreateIngredientPlan(out IngredientPlan plan, out string missingMessage) { plan = new IngredientPlan(); List<IngredientLocation> locations = CollectAllIngredientLocations(); int num = CountAvailableUnits(locations, 28); int num2 = CountAvailableUnits(locations, 72); int num3 = CountAvailableUnits(locations, 109); if (num < RequiredFireWoodCount || num2 < RequiredStoneCount || num3 < RequiredTorchCount) { missingMessage = "모닥불 점화 재료가 부족합니다.\n" + BuildMaterialProgressText(num, num2, num3); return false; } bool flag = TryAppendIngredientUnits(locations, 28, RequiredFireWoodCount, plan.FireWood); bool flag2 = TryAppendIngredientUnits(locations, 72, RequiredStoneCount, plan.Stone); bool flag3 = TryAppendIngredientUnits(locations, 109, RequiredTorchCount, plan.Torch); if (!flag || !flag2 || !flag3) { missingMessage = "재료 목록을 구성하는 동안 인벤토리가 변경되었습니다. 다시 시도하세요."; return false; } missingMessage = string.Empty; return true; } private static bool TryAppendIngredientUnits(List<IngredientLocation> locations, ushort itemId, int requiredCount, List<IngredientLocation> destination) { if (destination == null) { return false; } destination.Clear(); if (requiredCount <= 0) { return true; } List<IngredientLocation> list = new List<IngredientLocation>(); for (int i = 0; i < locations.Count; i++) { IngredientLocation ingredientLocation = locations[i]; if (ingredientLocation != null && ingredientLocation.ItemId == itemId && ingredientLocation.AvailableCount > 0) { list.Add(ingredientLocation); } } list.Sort(CompareIngredientLocations); int num = requiredCount; for (int j = 0; j < list.Count; j++) { if (num <= 0) { break; } IngredientLocation ingredientLocation2 = list[j]; int num2 = Mathf.Min(ingredientLocation2.AvailableCount, num); for (int k = 0; k < num2; k++) { destination.Add(ingredientLocation2); } num -= num2; } return num <= 0; } private static int CompareIngredientLocations(IngredientLocation left, IngredientLocation right) { int num = GetConsumptionPriority(left).CompareTo(GetConsumptionPriority(right)); if (num != 0) { return num; } int actorNumber = GetActorNumber(left); int actorNumber2 = GetActorNumber(right); int num2 = actorNumber.CompareTo(actorNumber2); if (num2 != 0) { return num2; } if (left.IsBackpackInternal != right.IsBackpackInternal) { if (!left.IsBackpackInternal) { return 1; } return -1; } if (left.IsBackpackInternal) { return left.BackpackSlotIndex.CompareTo(right.BackpackSlotIndex); } return left.ExternalSlotId.CompareTo(right.ExternalSlotId); } private static int GetActorNumber(IngredientLocation location) { if (location == null || (Object)(object)location.Character == (Object)null || (Object)(object)((MonoBehaviourPun)location.Character).photonView == (Object)null || ((MonoBehaviourPun)location.Character).photonView.Owner == null) { return int.MaxValue; } return ((MonoBehaviourPun)location.Character).photonView.Owner.ActorNumber; } private static List<IngredientLocation> CollectAllIngredientLocations() { List<IngredientLocation> list = new List<IngredientLocation>(); List<Character> allPlayerCharacters = PlayerHandler.GetAllPlayerCharacters(); for (int i = 0; i < allPlayerCharacters.Count; i++) { Character val = allPlayerCharacters[i]; if ((Object)(object)val == (Object)null || (Object)(object)val.player == (Object)null || (Object)(object)((MonoBehaviourPun)val).photonView == (Object)null || ((MonoBehaviourPun)val).photonView.Owner == null || ((MonoBehaviourPun)val).photonView.Owner.IsInactive) { continue; } Player player = val.player; ItemSlot[] itemSlots = player.itemSlots; if (itemSlots != null) { for (int j = 0; j < itemSlots.Length; j++) { AddIngredientLocation(list, player, val, itemSlots[j], isBackpackInternal: false, (byte)j, -1); } } AddIngredientLocation(list, player, val, player.tempFullSlot, isBackpackInternal: false, 250, -1); BackpackData val2 = null; if (player.backpackSlot != null && !((ItemSlot)player.backpackSlot).IsEmpty() && ((ItemSlot)player.backpackSlot).data != null && ((ItemSlot)player.backpackSlot).data.TryGetDataEntry<BackpackData>((DataEntryKey)7, ref val2) && val2 != null && val2.itemSlots != null) { for (int k = 0; k < val2.itemSlots.Length; k++) { AddIngredientLocation(list, player, val, val2.itemSlots[k], isBackpackInternal: true, byte.MaxValue, k); } } } return list; } private static void AddIngredientLocation(List<IngredientLocation> locations, Player player, Character character, ItemSlot slot, bool isBackpackInternal, byte externalSlotId, int backpackSlotIndex) { if (locations != null && !((Object)(object)player == (Object)null) && !((Object)(object)character == (Object)null) && slot != null && !slot.IsEmpty() && !((Object)(object)slot.prefab == (Object)null)) { ushort itemID = slot.prefab.itemID; if (itemID == 28 || itemID == 72 || itemID == 109) { locations.Add(new IngredientLocation { Player = player, Character = character, Slot = slot, IsBackpackInternal = isBackpackInternal, ExternalSlotId = externalSlotId, BackpackSlotIndex = backpackSlotIndex, ItemId = itemID, AvailableCount = GetLocationAvailableCount(player, slot, isBackpackInternal, externalSlotId) }); } } } private static int GetLocationAvailableCount(Player player, ItemSlot slot, bool isBackpackInternal, byte externalSlotId) { if (slot == null || slot.IsEmpty()) { return 0; } if (isBackpackInternal) { return 1; } int stackCount = InventoryStack.GetStackCount(player, externalSlotId); return Mathf.Max(1, stackCount); } private static int GetConsumptionPriority(IngredientLocation location) { if (location == null) { return int.MaxValue; } if (location.IsBackpackInternal) { return 0; } if (!IsCurrentlySelected(location)) { return 1; } return 2; } private static bool IsCurrentlySelected(IngredientLocation location) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) if (location == null || location.IsBackpackInternal || (Object)(object)location.Character == (Object)null || location.Character.refs == null || (Object)(object)location.Character.refs.items == (Object)null) { return false; } Optionable<byte> currentSelectedSlot = location.Character.refs.items.currentSelectedSlot; if (currentSelectedSlot.IsSome) { return currentSelectedSlot.Value == location.ExternalSlotId; } return false; } private static int CountAvailableUnits(List<IngredientLocation> locations, ushort itemId) { int num = 0; for (int i = 0; i < locations.Count; i++) { IngredientLocation ingredientLocation = locations[i]; if (ingredientLocation != null && ingredientLocation.ItemId == itemId) { num += Mathf.Max(0, ingredientLocation.AvailableCount); } } return num; } private static bool TryConsumeIngredientPlan(IngredientPlan plan, out List<ConsumedSelectedSlot> consumedSelectedSlots) { consumedSelectedSlots = new List<ConsumedSelectedSlot>(); if (plan == null || !ValidatePlannedUnits(plan.FireWood, 28, RequiredFireWoodCount) || !ValidatePlannedUnits(plan.Stone, 72, RequiredStoneCount) || !ValidatePlannedUnits(plan.Torch, 109, RequiredTorchCount)) { return false; } List<IngredientLocation> list = new List<IngredientLocation>(); list.AddRange(plan.FireWood); list.AddRange(plan.Stone); list.AddRange(plan.Torch); HashSet<Player> hashSet = new HashSet<Player>(); HashSet<Character> hashSet2 = new HashSet<Character>(); HashSet<string> hashSet3 = new HashSet<string>(); for (int i = 0; i < list.Count; i++) { IngredientLocation ingredientLocation = list[i]; if (!IsLocationStillValid(ingredientLocation, ingredientLocation.ItemId)) { return false; } if (IsCurrentlySelected(ingredientLocation) && (Object)(object)((MonoBehaviourPun)ingredientLocation.Character).photonView != (Object)null && ((MonoBehaviourPun)ingredientLocation.Character).photonView.Owner != null) { int actorNumber = ((MonoBehaviourPun)ingredientLocation.Character).photonView.Owner.ActorNumber; string item = actorNumber + ":" + ingredientLocation.ExternalSlotId; if (hashSet3.Add(item)) { consumedSelectedSlots.Add(new ConsumedSelectedSlot { ActorNumber = actorNumber, SlotId = ingredientLocation.ExternalSlotId }); } } ingredientLocation.Slot.EmptyOut(); hashSet.Add(ingredientLocation.Player); if (ingredientLocation.IsBackpackInternal) { hashSet2.Add(ingredientLocation.Character); } } foreach (Player item2 in hashSet) { SyncPlayerInventoryFromHost(item2); } foreach (Character item3 in hashSet2) { RefreshBackpackVisuals(item3); } RefreshAllCarryWeights(hashSet); return true; } private static bool ValidatePlannedUnits(List<IngredientLocation> plannedUnits, ushort expectedItemId, int expectedCount) { if (expectedCount <= 0) { if (plannedUnits != null) { return plannedUnits.Count == 0; } return false; } if (plannedUnits == null || plannedUnits.Count != expectedCount) { return false; } Dictionary<IngredientLocation, int> dictionary = new Dictionary<IngredientLocation, int>(); for (int i = 0; i < plannedUnits.Count; i++) { IngredientLocation ingredientLocation = plannedUnits[i]; if (!IsLocationStillValid(ingredientLocation, expectedItemId)) { return false; } if (!dictionary.ContainsKey(ingredientLocation)) { dictionary[ingredientLocation] = 0; } dictionary[ingredientLocation]++; } foreach (KeyValuePair<IngredientLocation, int> item in dictionary) { if (GetLocationAvailableCount(item.Key.Player, item.Key.Slot, item.Key.IsBackpackInternal, item.Key.ExternalSlotId) < item.Value) { return false; } } return true; } private static bool IsLocationStillValid(IngredientLocation location, ushort expectedItemId) { if (location != null && (Object)(object)location.Player != (Object)null && (Object)(object)location.Character != (Object)null && location.Slot != null && !location.Slot.IsEmpty() && (Object)(object)location.Slot.prefab != (Object)null) { return location.Slot.prefab.itemID == expectedItemId; } return false; } private static void SyncPlayerInventoryFromHost(Player player) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || !PhotonNetwork.IsMasterClient) { return; } PhotonView component = ((Component)player).GetComponent<PhotonView>(); if (!((Object)(object)component == (Object)null)) { InventorySyncData val = default(InventorySyncData); ((InventorySyncData)(ref val))..ctor(player.itemSlots, player.backpackSlot, player.tempFullSlot); component.RPC("SyncInventoryRPC", (RpcTarget)1, new object[2] { IBinarySerializable.ToManagedArray<InventorySyncData>(val), false }); if (player.itemsChangedAction != null) { player.itemsChangedAction(player.itemSlots); } } } private static void RefreshBackpackVisuals(Character character) { if (!((Object)(object)character == (Object)null) && PhotonNetwork.IsMasterClient) { CharacterBackpackHandler component = ((Component)character).GetComponent<CharacterBackpackHandler>(); if (!((Object)(object)component == (Object)null) && !((Object)(object)component.backpackVisuals == (Object)null)) { ((BackpackVisuals)component.backpackVisuals).RefreshVisuals(); } } } private static void RefreshAllCarryWeights(HashSet<Player> touchedPlayers) { if (touchedPlayers == null) { return; } foreach (Player touchedPlayer in touchedPlayers) { if (!((Object)(object)touchedPlayer == (Object)null) && !((Object)(object)touchedPlayer.character == (Object)null) && touchedPlayer.character.refs != null && !((Object)(object)touchedPlayer.character.refs.items == (Object)null)) { touchedPlayer.character.refs.items.RefreshAllCharacterCarryWeight(); break; } } } private void BroadcastConsumedSelectedSlots(List<ConsumedSelectedSlot> consumedSlots) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_007f: Unknown result type (might be due to invalid IL or missing references) if (consumedSlots != null && consumedSlots.Count != 0) { object[] array = new object[1 + consumedSlots.Count * 2]; array[0] = consumedSlots.Count; for (int i = 0; i < consumedSlots.Count; i++) { array[1 + i * 2] = consumedSlots[i].ActorNumber; array[2 + i * 2] = consumedSlots[i].SlotId; } RaiseEventOptions val = new RaiseEventOptions { Receivers = (ReceiverGroup)1 }; PhotonNetwork.RaiseEvent((byte)172, (object)array, val, SendOptions.SendReliable); } } private static void HandleConsumedSelectedSlots(object[] payload) { if (payload == null || payload.Length < 1 || PhotonNetwork.LocalPlayer == null) { return; } int num; try { num = Convert.ToInt32(payload[0]); } catch (Exception) { return; } int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber; for (int i = 0; i < num; i++) { int num2 = 1 + i * 2; int num3 = 2 + i * 2; if (num3 < payload.Length) { int num4; int consumedSlotId; try { num4 = Convert.ToInt32(payload[num2]); consumedSlotId = Convert.ToInt32(payload[num3]); } catch (Exception) { continue; } if (num4 == actorNumber) { UnequipConsumedLocalSlot(consumedSlotId); } continue; } break; } } private static void UnequipConsumedLocalSlot(int consumedSlotId) { //IL_0036: 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_005c: Unknown result type (might be due to invalid IL or missing references) Character localCharacter = Character.localCharacter; if (!((Object)(object)localCharacter == (Object)null) && localCharacter.refs != null && !((Object)(object)localCharacter.refs.items == (Object)null)) { Optionable<byte> currentSelectedSlot = localCharacter.refs.items.currentSelectedSlot; if (!currentSelectedSlot.IsNone && currentSelectedSlot.Value == (byte)consumedSlotId) { localCharacter.refs.items.EquipSlot(Optionable<byte>.None); } } } private void SendIgniteResult(int targetActorNumber, bool success, string message) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0057: Unknown result type (might be due to invalid IL or missing references) object[] array = new object[2] { success, message ?? string.Empty }; if (PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == targetActorNumber) { HandleIgniteResult(array); return; } RaiseEventOptions val = new RaiseEventOptions(); val.TargetActors = new int[1] { targetActorNumber }; RaiseEventOptions val2 = val; PhotonNetwork.RaiseEvent((byte)171, (object)array, val2, SendOptions.SendReliable); } private static void HandleIgniteResult(object[] resultData) { if (resultData == null || resultData.Length < 2) { return; } bool flag; string text; try { flag = Convert.ToBoolean(resultData[0]); text = resultData[1] as string; } catch (Exception) { return; } if (string.IsNullOrEmpty(text)) { text = (flag ? "모닥불을 점화했습니다." : "모닥불을 점화하지 못했습니다."); } NotifyLocalPlayer(text); if (ModLogger != null) { ModLogger.LogInfo((object)("[CampfireIgnitionDiag] Result received. Success=" + flag + " | Message=" + text.Replace("\n", " | "))); } if (ModLogger != null) { if (flag) { ModLogger.LogInfo((object)text.Replace("\n", " | ")); } else { ModLogger.LogWarning((object)text.Replace("\n", " | ")); } } } internal static void NotifyLocalPlayer(string message) { if (!string.IsNullOrEmpty(message)) { UI_Notifications val = Object.FindAnyObjectByType<UI_Notifications>(); if ((Object)(object)val != (Object)null) { val.AddNotification(message); } else if (ModLogger != null) { ModLogger.LogInfo((object)("Notification UI not found. Message=" + message.Replace("\n", " | "))); } } } internal static string BuildRequirementPrompt() { List<IngredientLocation> locations = CollectAllIngredientLocations(); int num = CountAvailableUnits(locations, 28); int num2 = CountAvailableUnits(locations, 72); int num3 = CountAvailableUnits(locations, 109); string text = ((num >= RequiredFireWoodCount && num2 >= RequiredStoneCount && num3 >= RequiredTorchCount) ? "#79E081" : "#FF8A80"); return "\n<color=" + text + ">필요: " + BuildMaterialProgressText(num, num2, num3) + "</color>"; } private static string BuildMaterialProgressText(int fireWoodCount, int stoneCount, int torchCount) { List<string> list = new List<string>(); if (RequiredFireWoodCount > 0) { list.Add("나뭇가지 " + Mathf.Min(fireWoodCount, RequiredFireWoodCount) + "/" + RequiredFireWoodCount); } if (RequiredStoneCount > 0) { list.Add("돌 " + Mathf.Min(stoneCount, RequiredStoneCount) + "/" + RequiredStoneCount); } if (RequiredTorchCount > 0) { list.Add("횃불 " + Mathf.Min(torchCount, RequiredTorchCount) + "/" + RequiredTorchCount); } if (list.Count == 0) { return "재료 없음"; } return string.Join(" | ", list.ToArray()); } private static string BuildConsumedMaterialMessage() { List<string> list = new List<string>(); if (RequiredFireWoodCount > 0) { list.Add("나뭇가지 " + RequiredFireWoodCount + "개"); } if (RequiredStoneCount > 0) { list.Add("돌 " + RequiredStoneCount + "개"); } if (RequiredTorchCount > 0) { list.Add("횃불 " + RequiredTorchCount + "개"); } if (list.Count == 0) { return "재료를 소비하지 않았습니다."; } return string.Join(", ", list.ToArray()) + "를 소비했습니다."; } private static int CountGroupIngredient(ushort itemId) { return CountAvailableUnits(CollectAllIngredientLocations(), itemId); } private static string BuildMaterialCountLog() { List<IngredientLocation> locations = CollectAllIngredientLocations(); return "FireWood=" + CountAvailableUnits(locations, 28) + ", Stone=" + CountAvailableUnits(locations, 72) + ", Torch=" + CountAvailableUnits(locations, 109) + " | Required=[" + RequiredFireWoodCount + ", " + RequiredStoneCount + ", " + RequiredTorchCount + "]"; } private static string DescribeLocation(IngredientLocation location) { if (location == null || (Object)(object)location.Character == (Object)null || (Object)(object)((MonoBehaviourPun)location.Character).photonView == (Object)null || ((MonoBehaviourPun)location.Character).photonView.Owner == null) { return "<unknown>"; } string nickName = ((MonoBehaviourPun)location.Character).photonView.Owner.NickName; if (location.IsBackpackInternal) { return nickName + ":Backpack[" + location.BackpackSlotIndex + "]"; } return nickName + ":Inventory[" + location.ExternalSlotId + "]"; } private static string StripRichTextForNotification(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } string text = value; for (int i = 0; i < 32; i++) { int num = text.IndexOf('<'); if (num < 0) { break; } int num2 = text.IndexOf('>', num); if (num2 < 0) { break; } text = text.Remove(num, num2 - num + 1); } return text.Trim(); } } [HarmonyPatch(typeof(Campfire), "Interact_CastFinished")] internal static class CampfireInteractCastFinishedPatch { [HarmonyPrefix] private static bool Prefix(Campfire __instance, Character interactor) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (!CampfireGate.IsManagedCampfire(__instance)) { return true; } if (__instance.Lit || (int)__instance.state != 0) { return true; } if ((Object)(object)CampfireGate.Instance != (Object)null) { CampfireGate.Instance.RequestIgnition(__instance, interactor); } return false; } } [HarmonyPatch(typeof(Campfire), "GetInteractionText")] internal static class CampfireGetInteractionTextPatch { [HarmonyPostfix] private static void Postfix(Campfire __instance, ref string __result) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (CampfireGate.IsManagedCampfire(__instance) && !__instance.Lit && (int)__instance.state == 0) { __result += CampfireGate.BuildRequirementPrompt(); } } } [BepInPlugin("com.sappheiros.crafting.shop", "Craft PEAK Unified Hub", "0.0.5")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class CraftHub : BaseUnityPlugin, IOnEventCallback, IInRoomCallbacks { internal enum HubTab { Description, Upgrade, Craft, Sell, Parts, Synthesis, Developer } internal enum HubLanguage { English, Korean, Chinese, Japanese, French } private sealed class UiTranslationEntry { public readonly string Source; public readonly string Korean; public readonly string English; public readonly string Chinese; public readonly string Japanese; public readonly string French; public UiTranslationEntry(string source, string korean, string english, string chinese, string japanese, string french) { Source = source ?? string.Empty; Korean = korean ?? string.Empty; English = english ?? string.Empty; Chinese = chinese ?? string.Empty; Japanese = japanese ?? string.Empty; French = french ?? string.Empty; } public string Get(HubLanguage language) { return language switch { HubLanguage.Korean => Korean, HubLanguage.Chinese => Chinese, HubLanguage.Japanese => Japanese, HubLanguage.French => French, _ => English, }; } } internal enum PendingRequest { None, Sell, Craft, Upgrade, Parts, Synthesis } internal enum RecipeTier { Basic, Standard, Advanced, Special, Masterwork } internal enum CraftUiCategory { Climbing, Food, Heal, Revive, Essential } internal sealed class IngredientCost { public ushort ItemId; public int Count; public IngredientCost(ushort itemId, int count) { ItemId = itemId; Count = Mathf.Max(1, count); } } internal sealed class CraftRecipe { public ushort OutputItemId; public Item OutputPrefab; public string DisplayName; public string Category; public RecipeTier Tier; public int RequiredResourceLevel; public int MoneyCost; public readonly List<IngredientCost> Ingredients = new List<IngredientCost>(); } internal sealed class SynthesisRecipe { public int Index; public string DisplayName; public readonly List<IngredientCost> Ingredients = new List<IngredientCost>(); public ushort[] OutputItemIds; public SynthesisRecipe(int index, string displayName, ushort[] outputItemIds, params IngredientCost[] ingredients) { Index = index; DisplayName = displayName ?? string.Empty; OutputItemIds = outputItemIds ?? Array.Empty<ushort>(); if (ingredients != null) { Ingredients.AddRange(ingredients); } } } private sealed class CampfireBuildRecipe { public int Stage; public string Name; public int RequiredResourceLevel; public int MoneyCost; public readonly List<IngredientCost> Ingredients = new List<IngredientCost>(); public CampfireBuildRecipe(int stage, string name, int requiredResourceLevel, int moneyCost, params IngredientCost[] ingredients) { Stage = Mathf.Clamp(stage, 1, 4); Name = name ?? string.Empty; RequiredResourceLevel = Mathf.Clamp(requiredResourceLevel, 0, 4); MoneyCost = Mathf.Max(0, moneyCost); if (ingredients != null) { Ingredients.AddRange(ingredients); } } } private sealed class PlanePartRecipe { public int Index; public string Name; public string Route; public int RequiredResourceLevel; public int MoneyCost; public readonly List<IngredientCost> Ingredients = new List<IngredientCost>(); public PlanePartRecipe(int index, string name, string route, int requiredResourceLevel, int moneyCost, params IngredientCost[] ingredients) { Index = index; Name = name ?? string.Empty; Route = route ?? string.Empty; RequiredResourceLevel = Mathf.Clamp(requiredResourceLevel, 0, 4); MoneyCost = Mathf.Max(0, moneyCost); if (ingredients != null) { Ingredients.AddRange(ingredients); } } } internal enum UpgradeKind { ResourceGrade, StackCapacity, CampfireEfficiency, DoubleYield, SellValue } private sealed class UpgradeFormulaConfig { public ConfigEntry<int> BaseCost; public ConfigEntry<int> CostGrowth; } private sealed class BalanceProfile { public readonly int Difficulty; public readonly string Name; public readonly int[] SalePrices; public readonly int[] CampfireMoneyCosts; public readonly int[] PartMoneyCosts; public readonly int FinalFlareMoneyCost; public readonly int CraftMoneyPercent; public readonly int UpgradeMoneyPercent; public readonly int ProgressionRemoveIngredients; public readonly int ProgressionBonusUnits; public readonly int CraftRemoveIngredients; public readonly int CraftBonusUnits; public readonly int CommonSynthesisTotal; public readonly int NormalSynthesisTotal; public readonly int RareSynthesisEach; public readonly int LegendarySynthesisCount; public BalanceProfile(int difficulty, string name, int[] salePrices, int[] campfireMoneyCosts, int[] partMoneyCosts, int finalFlareMoneyCost, int craftMoneyPercent, int upgradeMoneyPercent, int progressionRemoveIngredients, int progressionBonusUnits, int craftRemoveIngredients, int craftBonusUnits, int commonSynthesisTotal, int normalSynthesisTotal, int rareSynthesisEach, int legendarySynthesisCount) { Difficulty = Mathf.Clamp(difficulty, 1, 5); Name = name ?? string.Empty; SalePrices = salePrices ?? new int[5] { 100, 400, 1500, 5500, 20000 }; CampfireMoneyCosts = campfireMoneyCosts ?? new int[4] { 40000, 130000, 250000, 450000 }; PartMoneyCosts = partMoneyCosts ?? new int[4] { 30000, 100000, 210000, 350000 }; FinalFlareMoneyCost = Mathf.Max(0, finalFlareMoneyCost); CraftMoneyPercent = Mathf.Max(1, craftMoneyPercent); UpgradeMoneyPercent = Mathf.Max(1, upgradeMoneyPercent); ProgressionRemoveIngredients = Mathf.Max(0, progressionRemoveIngredients); ProgressionBonusUnits = Mathf.Max(0, progressionBonusUnits); CraftRemoveIngredients = Mathf.Max(0, craftRemoveIngredients); CraftBonusUnits = Mathf.Max(0, craftBonusUnits); CommonSynthesisTotal = Mathf.Max(1, commonSynthesisTotal); NormalSynthesisTotal = Mathf.Max(1, normalSynthesisTotal); RareSynthesisEach = Mathf.Max(1, rareSynthesisEach); LegendarySynthesisCount = Mathf.Max(1, legendarySynthesisCount); } } private sealed class UpgradeState { public int Protocol; public int Revision; public int OwnerActor; public string RunId; public int ResourceLevel; public int StackLevel; public int CampfireLevel; public int YieldMultiplier; public int SellMultiplier; public int BaseStackCount; public int[] BaseCampfireMaterials; public UpgradeState Clone() { return new UpgradeState { Protocol = Protocol, Revision = Revision, OwnerActor = OwnerActor, RunId = (RunId ?? string.Empty), ResourceLevel = ResourceLevel, StackLevel = StackLevel, CampfireLevel = CampfireLevel, YieldMultiplier = YieldMultiplier, SellMultiplier = SellMultiplier, BaseStackCount = BaseStackCount, BaseCampfireMaterials = CloneIntArray(BaseCampfireMaterials) }; } public static UpgradeState CreateDefault() { UpgradeState upgradeState = new UpgradeState(); upgradeState.Protocol = 1; upgradeState.Revision = 0; upgradeState.OwnerActor = 0; upgradeState.RunId = string.Empty; upgradeState.ResourceLevel = 4; upgradeState.StackLevel = 0; upgradeState.CampfireLevel = 0; upgradeState.YieldMultiplier = 1; upgradeState.SellMultiplier = 1; upgradeState.BaseStackCount = 10; upgradeState.BaseCampfireMaterials = new int[3] { 1, 1, 1 }; return upgradeState; } } private sealed class IngredientLocation { public Player Player; public Character Character; public ItemSlot Slot; public bool IsBackpackInternal; public byte ExternalSlotId; public int BackpackSlotIndex; public ushort ItemId; public int AvailableCount; } private sealed class PlannedIngredientUnit { public IngredientLocation Location; public ushort ItemId; } private sealed class CraftConsumptionPlan { public readonly List<PlannedIngredientUnit> Units = new List<PlannedIngredientUnit>(); } private struct ConsumedSelectedSlot { public int ActorNumber; public byte SlotId; } private sealed class PendingSaleTransaction { public int TransactionId; public int ActorNumber; public byte SlotId; public ushort ItemId; public string ItemGuid; public string ItemName; public int SalePrice; public double CreatedAt; } private sealed class LocalPendingSale { public int TransactionId; public byte SlotId; public ushort ItemId; public string ItemGuid; } internal struct PickupBonusState { public bool Eligible; public Player Player; public ushort ItemId; public int CountBefore; } private sealed class PoolRequest { public readonly List<ushort> Pool; public readonly int Count; public PoolRequest(List<ushort> pool, int count) { Pool = pool ?? new List<ushort>(); Count = Mathf.Max(1, count); } } private sealed class LiteTabView { private static readonly Color Normal = new Color(0.18f, 0.19f, 0.22f, 1f); private static readonly Color Selected = new Color(0.82f, 0.65f, 0.26f, 1f); private readonly HubTab tab; private readonly Button button; private readonly Image image; private readonly TextMeshProUGUI label; public LiteTabView(HubTab value, Button tabButton, Image tabImage, TextMeshProUGUI tabLabel) { tab = value; button = tabButton; image = tabImage; label = tabLabel; } public void Refresh(HubTab selectedTab) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) SetTextIfChanged(label, GetHubTabDisplayName(tab)); if ((Object)(object)image != (Object)null) { Color val = ((tab == selectedTab) ? Selected : Normal); if (((Graphic)image).color != val) { ((Graphic)image).color = val; } } SetInteractableIfChanged(button, tab != selectedTab); } } private sealed class LiteCraftCategoryView { private static readonly Color Normal = new Color(0.18f, 0.19f, 0.22f, 1f); private static readonly Color Selected = new Color(0.82f, 0.65f, 0.26f, 1f); private readonly CraftUiCategory category; private readonly Button button; private readonly Image image; private readonly TextMeshProUGUI label; public LiteCraftCategoryView(CraftUiCategory value, Button categoryButton, Image categoryImage, TextMeshProUGUI categoryLabel) { category = value; button = categoryButton; image = categoryImage; label = categoryLabel; } public void Refresh(bool visible, CraftUiCategory selectedCategory) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0053: 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) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) SetTextIfChanged(label, GetCraftUiCategoryName(category)); SetActiveIfChanged(((Object)(object)button != (Object)null) ? ((Component)button).gameObject : null, visible); if (!visible) { return; } if ((Object)(object)image != (Object)null) { Color val = ((category == selectedCategory) ? Selected : Normal); if (((Graphic)image).color != val) { ((Graphic)image).color = val; } } SetInteractableIfChanged(button, category != selectedCategory); } } private sealed class LiteLanguageView { private static readonly Color Normal = new Color(0.18f, 0.19f, 0.22f, 1f); private static readonly Color Selected = new Color(0.82f, 0.65f, 0.26f, 1f); private readonly HubLanguage language; private readonly Button button; private readonly Image image; public LiteLanguageView(HubLanguage value, Button languageButton, Image languageImage) { language = value; button = languageButton; image = languageImage; } public void Refresh(HubLanguage selectedLanguage) { //IL_001e: 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_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002a: 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) //IL_003d: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)image != (Object)null) { Color val = ((language == selectedLanguage) ? Selected : Normal); if (((Graphic)image).color != val) { ((Graphic)image).color = val; } } SetInteractableIfChanged(button, language != selectedLanguage); } } private sealed class LiteRowView { private static readonly Color Normal = new Color(0.14f, 0.15f, 0.18f, 1f); private static readonly Color Selected = new Color(0.82f, 0.65f, 0.26f, 1f); private readonly Button button; private readonly Image image; private readonly TextMeshProUGUI label; private readonly RawImage icon; public LiteRowView(Button rowButton, Image rowImage, TextMeshProUGUI rowLabel, RawImage rowIcon) { button = rowButton; image = rowImage; label = rowLabel; icon = rowIcon; } public void Refresh(bool active, bool selected, string text, Texture iconTexture = null, bool showIcon = false) { //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) SetActiveIfChanged(((Component)button).gameObject, active); if (!active) { if ((Object)(object)icon != (Object)null) { SetActiveIfChanged(((Component)icon).gameObject, active: false); } return; } if ((Object)(object)icon != (Object)null) { bool active2 = showIcon && (Object)(object)iconTexture != (Object)null; if ((Object)(object)icon.texture != (Object)(object)iconTexture) { icon.texture = iconTexture; } SetActiveIfChanged(((Component)icon).gameObject, active2); } if ((Object)(object)image != (Object)null) { Color val = (selected ? Selected : Normal); if (((Graphic)image).color != val) { ((Graphic)image).color = val; } } SetTextIfChanged(label, text); SetInteractableIfChanged(button, interactable: true); } } private sealed class CraftHubWindow : MenuWindow { private CraftHub owner; private List<LiteTabView> tabs = new List<LiteTabView>(); private List<LiteCraftCategoryView> craftCategoryTabs = new List<LiteCraftCategoryView>(); private List<LiteLanguageView> languages = new List<LiteLanguageView>(); private List<LiteRowView> rows = new List<LiteRowView>(); private TextMeshProUGUI title; private TextMeshProUGUI balance; private TextMeshProUGUI languageTitle; private TextMeshProUGUI help; private TextMeshProUGUI explanation; private GameObject detailPanel; private TextMeshProUGUI detail; private TextMeshProUGUI status; private TextMeshProUGUI actionLabel; private TextMeshProUGUI sellAllLabel; private TextMeshProUGUI page; private TextMeshProUGUI previousLabel; private TextMeshProUGUI nextLabel; private TextMeshProUGUI tempHandClearLabel; private TextMeshProUGUI closeLabel; private Button action; private Button sellAll; private Button previous; private Button next; private Button tempHandClear; private Button close; public override bool closeOnPause => true; public override bool closeOnUICancel => true; public override bool blocksPlayerInput => true; public override bool showCursorWhileOpen => true; public override Selectable objectToSelectOnOpen => (Selectable)(object)close; public void Initialize(CraftHub hub) { owner = hub; } public void SetReferences(List<LiteTabView> tabViews, List<LiteCraftCategoryView> craftCategoryViews, List<LiteLanguageView> languageViews, List<LiteRowView> rowViews, TextMeshProUGUI titleText, TextMeshProUGUI balanceText, TextMeshProUGUI languageTitleText, TextMeshProUGUI helpText, TextMeshProUGUI explanationText, GameObject detailPanelObject, TextMeshProUGUI detailText, TextMeshProUGUI statusText, Button actionButton, TextMeshProUGUI actionButtonLabel, Button sellAllButton, TextMeshProUGUI sellAllButtonLabel, TextMeshProUGUI pageText, Button previousButton, TextMeshProUGUI previousButtonLabel, Button nextButton, TextMeshProUGUI nextButtonLabel, Button tempHandClearButton, TextMeshProUGUI tempHandClearButtonLabel, Button closeButton, TextMeshProUGUI closeButtonLabel) { tabs = tabViews ?? new List<LiteTabView>(); craftCategoryTabs = craftCategoryViews ?? new List<LiteCraftCategoryView>(); languages = languageViews ?? new List<LiteLanguageView>(); rows = rowViews ?? new List<LiteRowView>(); title = titleText; balance = balanceText; languageTitle = languageTitleText; help = helpText; explanation = explanationText; detailPanel = detailPanelObject; detail = detailText; status = statusText; action = actionButton; actionLabel = actionButtonLabel; sellAll = sellAllButton; sellAllLabel = sellAllButtonLabel; page = pageText; previous = previousButton; previousLabel = previousButtonLabel; next = nextButton; nextLabel = nextButtonLabel; tempHandClear = tempHandClearButton; tempHandClearLabel = tempHandClearButtonLabel; close = closeButton; closeLabel = closeButtonLabel; } protected override void OnOpen() { RefreshContents(); } public void RefreshContents() { if (!((Object)(object)owner == (Object)null)) { HubTab currentTab = owner.currentTab; for (int i = 0; i < tabs.Count; i++) { tabs[i].Refresh(currentTab); } for (int j = 0; j < languages.Count; j++) { languages[j].Refresh(owner.currentLanguage); } SetTextIfChanged(languageTitle, "Language"); SetTextIfChanged(help, "P / ESC\n닫기"); SetTextIfChanged(previousLabel, "◀ 이전"); SetTextIfChanged(nextLabel, "다음 ▶"); SetTextIfChanged(closeLabel, "닫기"); SetTextIfChanged(tempHandClearLabel, "장착 해제"); SetInteractableIfChanged(tempHandClear, owner.CanClearLocalTempHandSlot); SetTextIfChanged(balance, "공유 잔액: " + owner.cachedSharedMoney + "원 | 난이도 " + owner.EffectiveBalanceDifficulty + " " + owner.EffectiveBalanceName); SetActiveIfChanged(((Object)(object)explanation != (Object)null) ? ((Component)explanation).gameObject : null, currentTab == HubTab.Description); SetActiveIfChanged(detailPanel, currentTab != HubTab.Description); for (int k = 0; k < craftCategoryTabs.Count; k++) { craftCategoryTabs[k].Refresh(currentTab == HubTab.Craft, owner.selectedCraftUiCategory); } switch (currentTab) { case HubTab.Description: RefreshDescription(); break; case HubTab.Upgrade: RefreshUpgrade(); break; case HubTab.Craft: RefreshCraft(); break; case HubTab.Synthesis: RefreshSynthesis(); break; case HubTab.Sell: RefreshSell(); break; case HubTab.Parts: RefreshParts(); break; case HubTab.Developer: RefreshDeveloper(); break; } } } private void RefreshDescription() { SetSellActionLayout(sellMode: false); SetTextIfChanged(title, "설명"); for (int i = 0; i < rows.Count; i++) { rows[i].Refresh(active: false, selected: false, string.Empty); } SetActiveIfChanged(((Component)page).gameObject, active: false); SetActiveIfChanged(((Component)previous).gameObject, active: false); SetActiveIfChanged(((Component)next).gameObject, active: false); SetActiveIfChanged(((Component)action).gameObject, active: false); SetTextIfChanged(explanation, BuildExplanationText()); } private void PrepareInteractiveTab() { SetSellActionLayout(sellMode: false); SetActiveIfChanged(((Component)action).gameObject, active: true); SetActiveIfChanged(detailPanel, active: true); } private void SetSellActionLayout(bool sellMode) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) RectTransform val = (((Object)(object)action != (Object)null) ? ((Component)action).GetComponent<RectTransform>() : null); if ((Object)(object)val != (Object)null) { val.anchoredPosition = new Vector2(sellMode ? (-90f) : 0f, 48f); val.sizeDelta = new Vector2(sellMode ? 170f : 350f, 66f); } SetActiveIfChanged(((Object)(object)sellAll != (Object)null) ? ((Component)sellAll).gameObject : null, sellMode); } private void RefreshUpgrade() { PrepareInteractiveTab(); SetTextIfChanged(title, "강화"); for (int i = 0; i < rows.Count; i++) { bool flag = i < VisibleUpgradeKinds.Length; UpgradeKind upgradeKind = ((!flag) ? UpgradeKind.StackCapacity : VisibleUpgradeKinds[i]); rows[i].Refresh(flag, flag && owner.selectedUpgradeKind == upgradeKind, flag ? owner.BuildUpgradeRowText(upgradeKind) : string.Empty); } SetActiveIfChanged(((Component)page).gameObject, active: false); SetActiveIfChanged(((Component)previous).gameObject, active: false); SetActiveIfChanged(((Component)next).gameObject, active: false); SetTextIfChanged(detail, BuildUpgradeDetailText(owner)); SetTextIfChanged(status, owner.upgradeStatus); bool flag2 = owner.SelectedUpgradeCurrentLevel >= owner.SelectedUpgradeMaximumLevel; SetInteractableIfChanged(action, owner.CanAttemptUpgrade); SetTextIfChanged(actionLabel, flag2 ? "최대 단계" : ((owner.pendingRequest == PendingRequest.Upgrade) ? "처리 중..." : "강화")); } private void RefreshCraft() { PrepareInteractiveTab(); SetTextIfChanged(title, "제작 · " + GetCraftUiCategoryName(owner.selectedCraftUiCategory)); CraftRecipe selectedCraftRecipe = owner.SelectedCraftRecipe; for (int i = 0; i < rows.Count; i++) { CraftRecipe craftRecipeAtCard = owner.GetCraftRecipeAtCard(i); rows[i].Refresh(craftRecipeAtCard != null, craftRecipeAtCard != null && selectedCraftRecipe != null && craftRecipeAtCard.OutputItemId == selectedCraftRecipe.OutputItemId, BuildCraftRowText(craftRecipeAtCard), GetCraftRecipeIcon(craftRecipeAtCard), showIcon: true); } SetActiveIfChanged(((Component)page).gameObject, active: true); SetActiveIfChanged(((Component)previous).gameObject, active: true); SetActiveIfChanged(((Component)next).gameObject, active: true); SetTextIfChanged(page, GetCraftUiCategoryName(owner.selectedCraftUiCategory) + " · 페이지 " + (owner.craftPage + 1) + " / " + owner.CraftTotalPages); SetInteractableIfChanged(previous, owner.craftPage > 0); SetInteractableIfChanged(next, owner.craftPage < owner.CraftTotalPages - 1); SetTextIfChanged(detail, BuildCraftDetailText(owner, selectedCraftRecipe, out var ready)); SetTextIfChanged(status, owner.craftStatus); SetInteractableIfChanged(action, selectedCraftRecipe != null && ready && owner.pendingRequest == PendingRequest.None); SetTextIfChanged(actionLabel, (owner.pendingRequest == PendingRequest.Craft) ? "처리 중..." : "제작 시도"); } private void RefreshSynthesis() { PrepareInteractiveTab(); SetTextIfChanged(title, "합성"); SynthesisRecipe selectedSynthesisRecipe = owner.SelectedSynthesisRecipe; for (int i = 0; i < rows.Count; i++) { SynthesisRecipe synthesisRecipeAtRow = owner.GetSynthesisRecipeAtRow(i); rows[i].Refresh(synthesisRecipeAtRow != null, synthesisRecipeAtRow != null && selectedSynthesisRecipe != null && synthesisRecipeAtRow.Index == selectedSynthesisRecipe.Index, owner.BuildSynthesisRowText(synthesisRecipeAtRow)); } SetActiveIfChanged(((Component)page).gameObject, active: false); SetActiveIfChanged(((Component)previous).gameObject, active: false); SetActiveIfChanged(((Component)next).gameObject, active: false); SetTextIfChanged(detail, owner.BuildSynthesisDetailText(selectedSynthesisRecipe, out var ready)); SetTextIfChanged(status, owner.synthesisStatus); SetInteractableIfChanged(action, selectedSynthesisRecipe != null && ready && owner.pendingRequest == PendingRequest.None); SetTextIfChanged(actionLabel, (owner.pendingRequest == PendingRequest.Synthesis) ? "처리 중..." : "합성 시도"); } private void RefreshSell() { PrepareInteractiveTab(); SetSellActionLayout(sellMode: true); SetTextIfChanged(title, "판매"); Player localPlayer = Player.localPlayer; int num = (((Object)(object)localPlayer != (Object)null && localPlayer.itemSlots != null) ? Mathf.Min(rows.Count, localPlayer.itemSlots.Length) : 0); for (int i = 0; i < rows.Count; i++) { bool flag = i < num; rows[i].Refresh(flag, flag && owner.selectedSellSlotId == i, flag ? BuildSellRowText(i) : string.Empty); } SetActiveIfChanged(((Component)page).gameObject, active: false); SetActiveIfChanged(((Component)previous).gameObject, active: false); SetActiveIfChanged(((Component)next).gameObject, active: false); bool canSell; string text = owner.BuildSelectedSellText(out canSell); SetTextIfChanged(detail, text + "\n\n가격표\n난이도 프리셋에 따라 판매가가 자동 조정됩니다."); SetTextIfChanged(status, owner.sellStatus); SetInteractableIfChanged(action, canSell && owner.pendingRequest == PendingRequest.None); SetTextIfChanged(actionLabel, (owner.pendingRequest == PendingRequest.Sell) ? "처리 중..." : (owner.selectedSellQuantity + "개 판매")); SetInteractableIfChanged(sellAll, canSell && owner.pendingRequest == PendingRequest.None); SetTextIfChanged(sellAllLabel, (owner.pendingRequest == PendingRequest.Sell) ? "처리 중..." : "일괄 판매"); } private void RefreshDeveloper() { PrepareInteractiveTab(); SetTextIfChanged(title, "개발자"); for (int i = 0; i < rows.Count; i++) { rows[i].Refresh(active: false, selected: false, string.Empty); } SetActiveIfChanged(((Component)page).gameObject, active: false); SetActiveIfChanged(((Component)previous).gameObject, active: false); SetActiveIfChanged(((Component)next).gameObject, active: false); SetTextIfChanged(detail, "개발자 테스트 치트\n\n클릭할 때마다 호스트에게 +10,000원 요청을 즉시 보냅니다.\n호스트는 각 요청을 독립적으로 순서대로 처리합니다.\n\n호스트 자신이 누른 경우에도 동일한 호스트 처리 경로를\n직접 실행하므로 요청이 누락되지 않습니다.\n최종 공유 돈은 Photon 방 전체에 동기화됩니다."); SetTextIfChanged(status, owner.developerStatus); SetInteractableIfChanged(action, PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null); SetTextIfChanged(actionLabel, "공유 돈 +10,000"); } private void RefreshParts() { PrepareInteractiveTab(); SetTextIfChanged(title, "부품"); for (int i = 0; i < rows.Count; i++) { bool flag = i < PlanePartRecipes.Count; rows[i].Refresh(flag, flag && owner.selectedPartIndex == i, flag ? owner.BuildPartRowText(i) : string.Empty); } SetActiveIfChanged(((Component)page).gameObject, active: false); SetActiveIfChanged(((Component)previous).gameObject, active: false); SetActiveIfChanged(((Component)next).gameObject, active: false); SetTextIfChanged(detail, owner.BuildPartDetailText(owner.SelectedPartRecipe, out var ready)); SetTextIfChanged(status, owner.partsStatus); SetInteractableIfChanged(action, ready); SetTextIfChanged(actionLabel, (owner.pendingRequest == PendingRequest.Parts) ? "처리 중..." : "부품 구매"); } } public const string PluginGuid = "com.sappheiros.crafting.shop"; public const string PluginName = "Craft PEAK Unified Hub"; public const string PluginVersion = "0.0.5"; public const string DeveloperName = "Sapphire009"; private const byte SellRequestEventCode = 140; private const byte SellResultEventCode = 141; private const byte SellConsumeRequestEventCode = 153; private const byte SellConsumeAckEventCode = 154; private const byte CraftRequestEventCode = 142; private const byte CraftResultEventCode = 143; private const byte ConsumedSlotEventCode = 144; private const byte UpgradeRequestEventCode = 145; private const byte UpgradeResultEventCode = 146; private const byte PartPurchaseRequestEventCode = 147; private const byte PartPurchaseResultEventCode = 148; private const byte ProgressionNoticeEventCode = 149; private const byte FinalFlareCompletedEventCode = 150; private const byte DeveloperMoneyRequestEventCode = 151; private const byte DeveloperMoneyResultEventCode = 152; private const byte SynthesisRequestEventCode = 155; private const byte SynthesisResultEventCode = 156; private const byte BackpackStackCorrectionEventCode = 157; private const byte BackpackStackSnapshotEventCode = 158; private const string SharedMoneyKey = "CraftPeak.SharedMoney"; private const string BalanceProtocolKey = "CraftPeak.Balance.Protocol"; private const string BalanceRunIdKey = "CraftPeak.Balance.RunId"; private const string BalanceDifficultyKey = "CraftPeak.Balance.Difficulty"; private const int BalanceProtocolVersion = 1; private const int CurrencyScale = 100; private const string PartsProtocolKey = "CraftPeak.Parts.Protocol"; private const string PartsRevisionKey = "CraftPeak.Parts.Revision"; private const string PartsRunIdKey = "CraftPeak.Parts.RunId"; private const string PartsPurchasedMaskKey = "CraftPeak.Parts.PurchasedMask"; private const string PartsConsumedMaskKey = "CraftPeak.Parts.ConsumedMask"; private const string PeakUnlockedKey = "CraftPeak.Parts.PeakUnlocked"; private const int PartsProtocolVersion = 1; private const string RunIdKey = "CraftPeak.Run.Id"; private const string RecipeProtocolKey = "CraftPeak.Recipes.Protocol"; private const string RecipeRunIdKey = "CraftPeak.Recipes.RunId"; private const string RecipeSeedKey = "CraftPeak.Recipes.Seed"; private const int RecipeProtocolVersion = 1; private const string UpgradeProtocolKey = "CraftPeak.Upgrade.Protocol"; private const string UpgradeRevisionKey = "CraftPeak.Upgrade.Revision"; private const string UpgradeOwnerKey = "CraftPeak.Upgrade.Owner"; private const string UpgradeRunIdKey = "CraftPeak.Upgrade.RunId"; private const string UpgradeResourceKey = "CraftPeak.Upgrade.Resource"; private const string UpgradeStackKey = "CraftPeak.Upgrade.Stack"; private const string UpgradeCampfireKey = "CraftPeak.Upgrade.Campfire"; private const string UpgradeYieldKey = "CraftPeak.Upgrade.Yield"; private const string UpgradeSellMultiplierKey = "CraftPeak.Upgrade.SellMultiplier"; private const string UpgradeBaseStackKey = "CraftPeak.Upgrade.BaseStack"; private const string UpgradeBaseCampfireKey = "CraftPeak.Upgrade.BaseCampfire"; private const int UpgradeProtocolVersion = 1; private const int ResourceUpgradeMaximum = 4; private const int StackUpgradeMaximum = 10; private const int DefaultBaseStackCount = 10; private const int CampfireUpgradeMaximum = 4; private const int YieldUpgradeMaximum = 10; private const int SellValueUpgradeMaximum = 10; private const double MinimumRequestIntervalSeconds = 0.25; private const float PartyResourceCacheSeconds = 1f; private static readonly int[] StackCapacityBonuses = new int[11] { 0, 5, 10, 20, 40, 55, 70, 90, 110, 135, 160 }; private static readonly int[] SellMultiplierByLevel = new int[11] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12 }; private static readonly float[] CampfireRequirementFactors = new float[4] { 1f, 0.9f, 0.78f, 0.65f }; private static readonly ConfigDefinition InventoryMaximumDefinition = new ConfigDefinition("01. 인벤토리 적재 설정", "슬롯당 최대 적재 수량"); private static readonly ConfigDefinition CampfireWoodDefinition = new ConfigDefinition("01. 캠프파이어 재료 조건", "나뭇가지 요구 수량"); private static readonly ConfigDefinition CampfireStoneDefinition = new ConfigDefinition("01. 캠프파이어 재료 조건", "돌 요구 수량"); private static readonly ConfigDefinition CampfireTorchDefinition = new ConfigDefinition("01. 캠프파이어 재료 조건", "횃불 요구 수량"); private const float RequestTimeoutSeconds = 6f; private const int CraftRecipesPerPage = 8; private const int MaximumVisibleInventorySlots = 8; private const ushort FireWoodItemId = 28; private const ushort StoneItemId = 72; private const ushort ConchItemId = 69; private const ushort BinocularsItemId = 14; private const ushort BingBongItemId = 13; private const ushort BugleItemId = 15; private const ushort FrisbeeItemId = 99; private const ushort GuidebookItemId = 34; private const ushort ScrollItemId = 49; private const ushort WeirdShroomItemId = 51; private const ushort StrangeGemItemId = 112; private const ushort TorchItemId = 109; private const ushort FlareItemId = 32; private const ushort ScoutEffigyItemId = 67; private static readonly ushort[] SaleResourceIds = new ushort[11] { 51, 28, 14, 13, 15, 69, 99, 34, 49, 72, 112 }; private static readonly ushort[] CraftIngredientIds = new ushort[12] { 28, 72, 69, 109, 14, 13, 15, 99, 34, 49, 51, 112 }; private static readonly ushort[] CommonIds = new ushort[3] { 28, 72, 69 }; private static readonly ushort[] NormalIds = new ushort[4] { 14, 13, 15, 99 }; private static readonly ushort[] RareIds = new ushort[2] { 34, 49 }; private static readonly List<PlanePartRecipe> PlanePartRecipes = new List<PlanePartRecipe>(); private static readonly List<CampfireBuildRecipe> NextCampfireRecipes = new List<CampfireBuildRecipe>(); private static bool progressionRecipesBuilt; private static string progressionRecipeRunId = string.Empty; private int sharedRecipeSeed; private string sharedRecipeRunId = string.Empty; private bool sharedRecipeSeedLoaded; private readonly List<CraftRecipe> craftRecipes = new List<CraftRecipe>(); private readonly Dictionary<ushort, CraftRecipe> craftRecipesByOutputId = new Dictionary<ushort, CraftRecipe>(); private static readonly List<SynthesisRecipe> SynthesisRecipes = new List<SynthesisRecipe>(); private static int synthesisRecipeDifficulty = -1; private readonly Dictionary<int, double> lastSellRequestAtByActor = new Dictionary<int, double>(); private readonly Dictionary<int, PendingSaleTransaction> pendingSaleTransactions = new Dictionary<int, PendingSaleTransaction>(); private readonly HashSet<string> reservedOrSoldItemGuids = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<string> pendingTempHandDrawGuids = new HashSet<string>(StringComparer.Ordinal); private LocalPendingSale localPendingSale; private int nextSaleTransactionId; private readonly Dictionary<int, double> lastCraftRequestAtByActor = new Dictionary<int, double>(); private readonly Dictionary<int, double> lastUpgradeRequestAtByActor = new Dictionary<int, double>(); private readonly Dictionary<int, double> lastSynthesisRequestAtByActor = new Dictionary<int, double>(); private readonly Dictionary<int, double> lastPartRequestAtByActor = new Dictionary<int, double>(); private CraftHubWindow activeWindow; private HubTab currentTab; private HubLanguage currentLanguage; private UpgradeKind selectedUpgradeKind = UpgradeKind.StackCapacity; private int selectedCraftRecipeIndex = -1; private int selectedSynthesisRecipeIndex; private int craftPage; private CraftUiCategory selectedCraftUiCategory; private int selectedSellSlotId = -1; private int selectedSellQuantity = 1; private int selectedPartIndex; private PendingRequest pendingRequest; private float requestStartedAt; private bool waitingForNewRun = true; private int cachedSharedMoney; private UpgradeState upgradeState = UpgradeState.CreateDefault(); private bool upgradeStateLoaded; private bool gameplayScene; private bool pendingFreshUpgradeRun = true; private float partyResourceCacheUntil; private readonly Dictionary<ushort, int> cachedPartyResourceCounts = new Dictionary<ushort, int>(); private static readonly Dictionary<string, int> correctedBackpackStackCountsByGuid = new Dictionary<string, int>(StringComparer.Ordinal); private static readonly HashSet<ItemSlot> correctedBackpackSlots = new HashSet<ItemSlot>(); private readonly StringBuilder sharedTextBuilder = new StringBuilder(384); private static TMP_FontAsset cachedFontAsset; private int partsRevision; private string partsRunId = string.Empty; private int purchasedPartsMask; private int consumedPartsMask; private bool peakUnlocked; private bool partsStateLoaded; private bool upgradeRoomStateDirty = true; private bool partsRoomStateDirty = true; private int lastAppliedUpgradeRevision = -1; private string lastAppliedUpgradeRunId = string.Empty; private UpgradeFormulaConfig stackUpgradeFormula; private UpgradeFormulaConfig campfireUpgradeFormula; private ConfigEntry<int> balanceDifficultyConfig; private int effectiveBalanceDifficulty = 3; private string balanceRunId = string.Empty; private bool balanceStateLoaded; private bool balanceRoomStateDirty = true; private ConfigEntry<int> doubleYieldCostConfig; private UpgradeFormulaConfig sellValueUpgradeFormula; private string upgradeStatus = "강화 항목을 선택하세요."; private string craftStatus = "제작할 아이템을 선택하세요."; private string synthesisStatus = "합성할 단계를 선택하세요."; private string sellStatus = "판매할 인벤토리 슬롯을 선택하세요."; private string partsStatus = "현재 세그먼트에 필요한 비행기 부품을 구매하세요."; private string developerStatus = "버튼을 누르면 로컬에서 +10,000원이 누적된 뒤 묶어서 공유됩니다."; private int developerMoneyRequestSequence; private int developerMoneyPendingResponses; private int developerHostAuthoritativeBalance = -1; private const int DeveloperMoneyPerClick = 10000; private static readonly UiTranslationEntry[] UiTranslationEntries = new UiTranslationEntry[476] { new UiTranslationEntry("본인 인벤토리의 합성 재료와 결과 공간을 확인하세요.", "본인 인벤토리의 합성 재료와 결과 공간을 확인하세요.", "Check your own inventory materials and result space.", "请检查自己背包中的合成材料和结果空间。", "自分のインベントリ内の合成素材と結果用スペースを確認してください。", "Vérifiez les matériaux de votre inventaire et l’espace pour le résultat."), new UiTranslationEntry("합성 결과 지급 중 오류가 발생했습니다. 재료는 복구되지 않았습니다.", "합성 결과 지급 중 오류가 발생했습니다. 재료는 복구되지 않았습니다.", "An error occurred while delivering the synthesis result. Materials were not restored.", "发放合成结果时发生错误,材料未恢复。", "合成結果の付与中にエラーが発生しました。素材は復元されませんでした。", "Une erreur est survenue lors de la remise du résultat de synthèse. Les matériaux n’ont pas été restaurés."), new UiTranslationEntry("합성 단계 번호를 해석하지 못했습니다.", "합성 단계 번호를 해석하지 못했습니다.", "Could not read the synthesis tier number.", "无法解析合成阶段编号。", "合成段階番号を解析できませんでした。", "Impossible d’interpréter le numéro du niveau de synthèse."), new UiTranslationEntry("등록되지 않은 합성식입니다.", "등록되지 않은 합성식입니다.", "This synthesis recipe is not registered.", "该合成配方未注册。", "未登録の合成レシピです。", "Cette recette de synthèse n’est pas enregistrée."), new UiTranslationEntry("잘못된 합성 요청입니다.", "잘못된 합성 요청입니다.", "Invalid synthesis request.", "合成请求无效。", "合成リクエストが不正です。", "Demande de synthèse invalide."), new UiTranslationEntry("현재는 합성할 수 없습니다.", "현재는 합성할 수 없습니다.", "Synthesis is not currently available.", "当前无法合成。", "現在は合成できません。", "La synthèse n’est pas disponible actuellement."), new UiTranslationEntry("합성 결과 지급 중 오류가 발생했습니다.", "합성 결과 지급 중 오류가 발생했습니다.", "An error occurred while delivering the synthesis result.", "发放合成结果时发生错误。", "合成結果の付与中にエラーが発生しました。", "Une erreur est survenue lors de la remise du résultat de synthèse."), new UiTranslationEntry("합성 결과를 받을 인벤토리 공간이 없습니다.", "합성 결과를 받을 인벤토리 공간이 없습니다.", "There is no inventory space for the synthesis result.", "背包中没有空间容纳合成结果。", "合成結果を受け取るインベントリ空間がありません。", "Il n’y a pas de place dans l’inventaire pour le résultat de synthèse."), new UiTranslationEntry("합성 재료 소비 중 인벤토리가 변경되었습니다. 다시 시도하세요.", "합성 재료 소비 중 인벤토리가 변경되었습니다. 다시 시도하세요.", "The inventory changed while consuming synthesis materials. Try again.", "消耗合成材料时背包发生变化,请重试。", "合成素材の消費中にインベントリが変化しました。もう一度お試しください。", "L’inventaire a changé pendant la consommation des matériaux de synthèse. Réessayez."), new UiTranslationEntry("합성 결과 데이터가 올바르지 않습니다.", "합성 결과 데이터가 올바르지 않습니다.", "The synthesis result data is invalid.", "合成结果数据无效。", "合成結果データが不正です。", "Les données du résultat de synthèse sont invalides."), new UiTranslationEntry("합성 결과를 해석하지 못했습니다.", "합성 결과를 해석하지 못했습니다.", "Could not read the synthesis result.", "无法解析合成结果。", "合成結果を解析できませんでした。", "Impossible d’interpréter le résultat de synthèse."), new UiTranslationEntry("합성 요청 전송에 실패했습니다.", "합성 요청 전송에 실패했습니다.", "Failed to send the synthesis request.", "发送合成请求失败。", "合成リクエストの送信に失敗しました。", "Échec de l’envoi de la demande de synthèse."), new UiTranslationEntry("합성 요청이 너무 빠릅니다.", "합성 요청이 너무 빠릅니다.", "Synthesis requests are being sent too quickly.", "合成请求过于频繁。", "合成リクエストが早すぎます。", "Les demandes de synthèse sont trop rapides."), new UiTranslationEntry("합성할 단계를 선택하세요.", "합성할 단계를 선택하세요.", "Select a synthesis tier.", "请选择合成阶段。", "合成段階を選択してください。", "Sélectionnez un niveau de synthèse."), new UiTranslationEntry("합성 요청이 거부되었습니다.", "합성 요청이 거부되었습니다.", "The synthesis request was rejected.", "合成请求被拒绝。", "合成リクエストが拒否されました。", "La demande de synthèse a été refusée."), new UiTranslationEntry("합성에 성공했습니다.", "합성에 성공했습니다.", "Synthesis succeeded.", "合成成功。", "合成に成功しました。", "Synthèse réussie."), new UiTranslationEntry("합성을 요청했습니다...", "합성을 요청했습니다...", " synthesis requested...", " 已请求合成……", "の合成をリクエストしました...", " : synthèse demandée..."), new UiTranslationEntry("망원경 합성", "망원경 합성", "Binoculars Synthesis", "双筒望远镜合成", "双眼鏡合成", "Synthèse de jumelles"), new UiTranslationEntry("빙봉 합성", "빙봉 합성", "Bing Bong Synthesis", "冰棒合成", "ビン・ボン合成", "Synthèse de Bing Bong"), new UiTranslationEntry("나팔 합성", "나팔 합성", "Bugle Synthesis", "号角合成", "ラッパ合成", "Synthèse de clairon"), new UiTranslationEntry("플라잉 디스크 합성", "플라잉 디스크 합성", "Frisbee Synthesis", "飞盘合成", "フリスビー合成", "Synthèse de frisbee"), new UiTranslationEntry("일반 자원 합성", "일반 자원 합성", "Common Resource Synthesis", "普通资源合成", "コモン資源合成", "Synthèse de ressources communes"), new UiTranslationEntry("보통 자원 합성", "보통 자원 합성", "Normal Resource Synthesis", "标准资源合成", "ノーマル資源合成", "Synthèse de ressources normales"), new UiTranslationEntry("희귀 자원 합성", "희귀 자원 합성", "Rare Resource Synthesis", "稀有资源合成", "レア資源合成", "Synthèse de ressources rares"), new UiTranslationEntry("괴상 버섯 압축 합성", "괴상 버섯 압축 합성", "Weird Shroom Compression", "怪异蘑菇压缩合成", "奇妙なキノコ圧縮合成", "Compression de champignons étranges"), new UiTranslationEntry("합성 결과", "합성 결과", "Synthesis result", "合成结果", "合成結果", "Résultat de synthèse"), new UiTranslationEntry("무작위 결과 후보", "무작위 결과 후보", "Random result pool", "随机结果候选", "ランダム結果候補", "Résultats aléatoires possibles"), new UiTranslationEntry("필요 재료", "필요 재료", "Required materials", "所需材料", "必要素材", "Matériaux requis"), new UiTranslationEntry("합성 시도", "합성 시도", "Synthesize", "合成", "合成する", "Synthétiser"), new UiTranslationEntry("합성", "합성", "Synthesis", "合成", "合成", "Synthèse"), new UiTranslationEntry("모닥불 점화 조건 미충족\nP 메뉴 강화 탭의 다음 모닥불 제작에서 ", "모닥불 점화 조건 미충족\nP 메뉴 강화 탭의 다음 모닥불 제작에서 ", "Campfire requirements not met\nIn the P menu's Upgrades tab, the level ", "未满足篝火点燃条件\n请先在 P 菜单的升级标签中制作第", "焚き火の点火条件を満たしていません\nPメニューの強化タブで", "Conditions d’allumage du feu de camp non remplies\nDans l’onglet Améliorations du menu P, le niveau "), new UiTranslationEntry("모닥불 점화 조건 미충족\nP 메뉴의 부품 탭에서 현재 구간 비행기 부품을 먼저 구매하세요.", "모닥불 점화 조건 미충족\nP 메뉴의 부품 탭에서 현재 구간 비행기 부품을 먼저 구매하세요.", "Campfire requirements not met\nPurchase the aircraft parts for the current segment from the Parts tab in the P menu first.", "未满足篝火点燃条件\n请先在 P 菜单的部件标签中购买当前区域所需的飞机部件。", "焚き火の点火条件を満たしていません\nPメニューの部品タブで現在の区間に必要な飛行機部品を先に購入してください。", "Conditions d’allumage du feu de camp non remplies\nAchetez d’abord les pièces d’avion du segment actuel dans l’onglet Pièces du menu P."), new UiTranslationEntry("최종 조명탄 제작에는 Legendary 제작 등급이 필요합니다.", "최종 조명탄 제작에는 전설 제작 등급이 필요합니다.", "The Legendary crafting grade is required to craft the final flare.", "制作最终信号弹需要传说制作等级。", "最終フレアの作成にはレジェンダリークラフト等級が必要です。", "Le niveau de fabrication Légendaire est requis pour fabriquer la fusée finale."), new UiTranslationEntry("가마 구간은 비행기 부품과 모닥불 진행 대상이 아닙니다.\n정상에 도착한 뒤 P 메뉴의 제작 탭에서 최종 조명탄을 제작하세요.", "가마 구간은 비행기 부품과 모닥불 진행 대상이 아닙니다.\n정상에 도착한 뒤 P 메뉴의 제작 탭에서 최종 조명탄을 제작하세요.", "The Kiln segment does not use aircraft parts or campfire progression.\nAfter reaching the Peak, craft the final flare from the Crafting tab in the P menu.", "熔炉区域不使用飞机部件或篝火推进。\n到达山顶后,在 P 菜单的制作标签中制作最终信号弹。", "窯区間では飛行機部品と焚き火進行を使用しません。\n山頂に到達したら、Pメニューのクラフトタブで最終フレアを作成してください。", "Le segment Four n’utilise ni pièces d’avion ni progression par feu de camp.\nAprès avoir atteint le Sommet, fabriquez la fusée finale dans l’onglet Fabrication du menu P."), new UiTranslationEntry("새 등반의 비행기 부품 상태가 아직 초기화되지 않았습니다.\nP 메뉴의 부품 탭을 열어 진행 상태를 초기화하세요.", "새 등반의 비행기 부품 상태가 아직 초기화되지 않았습니다.\nP 메뉴의 부품 탭을 열어 진행 상태를 초기화하세요.", "The aircraft-part state for the new climb has not been initialized.\nOpen the Parts tab in the P menu to initialize progression.", "新攀登的飞机部件状态尚未初始化。\n打开 P 菜单中的部件标签以初始化进度。", "新しい登山の飛行機部品状態がまだ初期化されていません。\nPメニューの部品タブを開いて進行状態を初期化してください。", "L’état des pièces d’avion de la nouvelle ascension n’est pas initialisé.\nOuvrez l’onglet Pièces du menu P pour initialiser la progression."), new UiTranslationEntry("\n\n가격표\n난이도 프리셋에 따라 판매가가 자동 조정됩니다.", "\n\n가격표\n난이도 프리셋에 따라 판매가가 자동 조정됩니다.", "\n\nPrice list\nSale values are adjusted by the host difficulty preset.", "\n\n价格表\n出售价格会根据房主的难度预设自动调整。", "\n\n価格表\n売却価格はホストの難易度プリセットに応じて自動調整されます。", "\n\nTarifs\nLes valeurs de vente suivent le préréglage de difficulté de l’hôte."), new UiTranslationEntry("부품 상태 저장에 실패했습니다. 공유 돈은 환불되었지만 재료는 복구되지 않았습니다.", "부품 상태 저장에 실패했습니다. 공유 돈은 환불되었지만 재료는 복구되지 않았습니다.", "Failed to save the part state. Shared money was refunded, but materials were not restored.", "保存部件状态失败。共享资金已退还,但材料未恢复。", "部品状態の保存に失敗しました。共有資金は返金されましたが、素材は復元されませんでした。", "Échec de l’enregistrement de l’état de la pièce. L’argent partagé a été remboursé, mais les matériaux n’ont pas été restaurés."), new UiTranslationEntry("구매 완료.\n인벤토리에는 들어가지 않으며 모닥불 진행 조건으로 저장됩니다.", "구매 완료.\n인벤토리에는 들어가지 않으며 모닥불 진행 조건으로 저장됩니다.", "Purchase complete.\nThe part is stored as a campfire progression requirement and does not enter the inventory.", "购买完成。\n部件不会进入背包,而是保存为篝火推进条件。", "購入完了。\nインベントリには入らず、焚き火の進行条件として保存されます。", "Achat terminé.\nLa pièce est enregistrée comme condition de progression du feu de camp et n’entre pas dans l’inventaire."), new UiTranslationEntry("판매 처리 중 인벤토리가 변경되어 요청한 수량을 모두 제거하지 못했습니다.", "판매 처리 중 인벤토리가 변경되어 요청한 수량을 모두 제거하지 못했습니다.", "The inventory changed during the sale, so the full requested quantity could not be removed.", "出售过程中背包发生变化,无法移除全部请求数量。", "売却処理中にインベントリが変化し、要求数量をすべて削除できませんでした。", "L’inventaire a changé pendant la vente ; toute la quantité demandée n’a pas pu être retirée."), new UiTranslationEntry("버튼을 누르면 로컬에서 +10,000원이 누적된 뒤 묶어서 공유됩니다.", "버튼을 누르면 로컬에서 +10,000원이 누적된 뒤 묶어서 공유됩니다.", "Press the button to add +10,000 shared money.", "按下按钮可增加 10,000 共享资金。", "ボタンを押すと共有資金が10,000増えます。", "Appuyez sur le bouton pour ajouter 10 000 à l’argent partagé."), new UiTranslationEntry("현재 구간의 모닥불만 다음 세그먼트 진행에 사용할 수 있습니다.", "현재 구간의 모닥불만 다음 세그먼트 진행에 사용할 수 있습니다.", "Only the current segment’s campfire can advance to the next segment.", "只有当前区域的篝火可用于推进到下一区域。", "現在の区間の焚き火のみ次の区間への進行に使用できます。", "Seul le feu de camp du segment actuel permet de progresser vers le segment suivant."), new UiTranslationEntry("번째 다음 모닥불 제작에는 다음 비행기 모듈 구매가 필요합니다.", "번째 다음 모닥불 제작에는 다음 비행기 모듈 구매가 필요합니다.", ": crafting the next campfire requires the following aircraft modules.", ":制作后续篝火需要以下飞机模块。", "番目の次の焚き火作成には以下の飛行機モジュールが必要です。", " : la fabrication du prochain feu de camp exige les modules d’avion suivants."), new UiTranslationEntry("호스트가 이번 판 제작식과 부품 재료를 확정하는 중입니다...", "호스트가 이번 판 제작식과 부품 재료를 확정하는 중입니다...", "The host is finalizing this run’s recipes and part materials...", "主机正在确定本局的配方和部件材料……", "ホストが今回のレシピと部品素材を確定しています...", "L’hôte finalise les recettes et les matériaux des pièces de cette partie..."), new UiTranslationEntry("정상에서 최종 조명탄 제작 완료. 탈출 신호를 발사했습니다.", "정상에서 최종 조명탄 제작 완료. 탈출 신호를 발사했습니다.", "Final flare crafted at the Peak. The escape signal has been launched.", "已在山顶制作最终信号弹,并发射逃脱信号。", "山頂で最終フレアを作成し、脱出信号を発射しました。", "Fusée finale fabriquée au Sommet. Le signal d’évacuation a été lancé."), new UiTranslationEntry("가마 이후 정상에 도착하면 P → 제작 → 최종 조명탄 제작", "가마 이후 정상에 도착하면 P → 제작 → 최종 조명탄 제작", "After the Kiln, reach the Peak and choose P → Crafting → Final Flare", "熔炉之后到达山顶,选择 P → 制作 → 最终信号弹", "窯の後に山頂へ到達し、P → クラフト → 最終フレア", "Après le Four, atteignez le Sommet puis P → Fabrication → Fusée finale"), new UiTranslationEntry("클릭할 때마다 호스트에게 +10,000원 요청을 즉시 보냅니다.\n", "클릭할 때마다 호스트에게 +10,000원 요청을 즉시 보냅니다.\n", "Each click immediately sends a +10,000 shared-money request to the host.\n", "每次点击都会立即向主机发送增加