설명을 뒷받침하는
구현과 검증 범위.
2026.09.11 소스 스냅샷 · 프로젝트 상대 경로와 원문 줄 번호를 함께 제공합니다. 전체 저장소가 아닌 핵심 범위 발췌입니다.
검증 기록은 범위별로 읽습니다
| 기록 | 확인한 범위 | 해석의 경계 |
|---|---|---|
| 44종 · 2026.09.07 | 제작 자료의 Catalog·Draft 편입 대조 | 제작 도구 적용 규모. 작업 시간 개선율이나 아트 원작 제작 수가 아닙니다. |
| 270/270 · 2026.08.29 가이드 | 9개 테마 × 10개 난이도 × 3개 시드의 생성 검증 집계 | 해당 입력 조합의 기록. 모든 시드·밸런스·재미를 보장하지 않습니다. |
| EditMode 126/126 · PlayMode 3/3 | 기존 Hex 검증 집계 | 집계와 모든 개별 assertion의 대응은 여기서 제공하지 않습니다. Writer·저장 실패 주입의 통과 수로 사용하지 않습니다. |
| 아래 선택 소스 | 생산 검증·비용 지도·후보 저장·정산 가드의 구현 | 소스 경로 확인이며 실기기·장시간·오류 주입 실행 결과는 아닙니다. |
집계가 수록된 팀 발표 원본 ↗ · 팀 QA와 JIRA 집계는 협업 기록입니다.
추가 검증 과제: Writer 실패 지점별 복구, 플랫폼별 저장 중단·복구, 병력 수별 프레임 시간과 비용 지도 캐시 적중률.
팀의 제작 편차를 공통 생산 흐름으로
설계와 실행 흐름 읽기 ↗생산 전 검증과 변경 범위 확보
Assets/ProjectMT/Editor/MonsterMaker/MonsterMakerAssetWriter.cs · L57–112원본 파일 식별값 · SHA-256
7f7d5192f627f22d5f93d617fbfcef5d79746323ab9dea8bd462450cee68668057public static MonsterMakerWriteResult BuildAndRegister(58MonsterMakerDraft draft,59MonsterCatalog catalog = null,60MonsterRarityCatalog rarityCatalog = null)61{62draft?.EditorSyncActiveAttackAuthoring();63var preflight = MonsterMakerValidator.Validate(draft);64if (preflight.HasErrors)65{66throw new InvalidOperationException(BuildIssueText(preflight.Issues));67}6869catalog ??= AssetDatabase.LoadAssetAtPath<MonsterCatalog>(MonsterCatalogPath);70rarityCatalog ??= AssetDatabase.LoadAssetAtPath<MonsterRarityCatalog>(MonsterRarityCatalogPath);71if (catalog == null || rarityCatalog == null)72{73throw new InvalidOperationException("MonsterCatalog 또는 MonsterRarityCatalog을 찾을 수 없습니다.");74}7576var paths = BuildPaths(draft.MonsterId);77var generatesUniquePassive = draft.UsePassiveSkill &&78draft.RarityPassiveSkill is GenericMonsterPassiveSkill;79var generatesAttackActive = draft.UseActiveSkill &&80draft.Rarity >= MonsterRarity.Legendary &&81draft.HasActiveProfile;82var passivePath = BuildPassivePath(draft.MonsterId);83var extraPaths = new List<string>();84if (generatesUniquePassive) extraPaths.Add(passivePath);85if (generatesAttackActive) extraPaths.Add(BuildActivePath(draft.MonsterId));86var outputPaths = paths.Concat(extraPaths).ToArray();87var dataFolder = DataRoot + "/" + draft.MonsterId;88var artFolder = ArtRoot + "/" + draft.MonsterId;89var catalogPath = RequirePersistentAssetPath(catalog, "MonsterCatalog");90var rarityCatalogPath = RequirePersistentAssetPath(rarityCatalog, "MonsterRarityCatalog");91ValidateProductionDraftOwnership(draft, catalogPath, rarityCatalogPath);92var writesProductionAiCatalog =93string.Equals(catalogPath, MonsterCatalogPath, StringComparison.OrdinalIgnoreCase) &&94string.Equals(rarityCatalogPath, MonsterRarityCatalogPath, StringComparison.OrdinalIgnoreCase);95var transactionPaths = outputPaths.Concat(new[] { catalogPath, rarityCatalogPath });96if (writesProductionAiCatalog)97{98transactionPaths = transactionPaths.Concat(new[]99{100CastleRaidAIProfileCatalogPath,101MainBattleAIProfileCatalogPath102});103}104var transaction = MonsterMakerWriteTransaction.Capture(105transactionPaths,106new[] { dataFolder, artFolder });107108try109{110EnsureFolder("Assets/ProjectMT/02_Shared/Unit/Data", "Monsters");111EnsureFolder(DataRoot, draft.MonsterId);112EnsureFolder("Assets/ProjectMT/05_Art", "Monsters");
생성물·Catalog·GUID 검증과 복구
Assets/ProjectMT/Editor/MonsterMaker/MonsterMakerAssetWriter.cs · L415–485원본 파일 식별값 · SHA-256
7f7d5192f627f22d5f93d617fbfcef5d79746323ab9dea8bd462450cee686680415definition = AssetDatabase.LoadAssetAtPath<MonsterDefinition>(paths[0]);416var outputValidation = MonsterDefinitionValidator.Validate(definition, true);417if (outputValidation.HasErrors)418{419throw new InvalidOperationException(BuildRuntimeIssueText(outputValidation.Issues));420}421422RegisterLast(catalog, rarityCatalog, definition, draft, uniquePassive, attackActive);423SaveAssetsIfDirty(catalog, rarityCatalog, castleRaidAiCatalog, mainBattleAiCatalog);424AssetDatabase.Refresh();425426if (!catalog.TryGet(draft.MonsterId, out var registered) || registered != definition)427{428throw new InvalidOperationException("생성물 검증 뒤 MonsterCatalog 등록을 확인하지 못했습니다.");429}430431if (!rarityCatalog.TryGetRarity(draft.MonsterId, out var rarity) || rarity != draft.Rarity)432{433throw new InvalidOperationException("생성물 검증 뒤 MonsterRarityCatalog 등록을 확인하지 못했습니다.");434}435436var assignedAiProfile = castleRaidAiCatalog?.Resolve(draft.MonsterId);437if (castleRaidAiCatalog != null && (assignedAiProfile == null ||438assignedAiProfile.Pattern != draft.CastleRaidAiPattern))439{440throw new InvalidOperationException("생성물 검증 뒤 Hex Castle Raid AI Profile 등록을 확인하지 못했습니다.");441}442443if (mainBattleAiCatalog != null &&444(!mainBattleAiCatalog.TryResolve(draft.MonsterId, out var mainBattleProfile) ||445mainBattleProfile.Role != draft.MainBattleRole ||446mainBattleProfile.TargetPriority != draft.MainBattleTargetPriority))447{448throw new InvalidOperationException("생성물 검증 뒤 MainBattle AI Profile 등록을 확인하지 못했습니다.");449}450451var guidAfter = outputPaths.ToDictionary(path => path, AssetDatabase.AssetPathToGUID);452for (var index = 0; index < outputPaths.Length; index++)453{454var before = guidBefore[outputPaths[index]];455if (!string.IsNullOrEmpty(before) && !string.Equals(before, guidAfter[outputPaths[index]], StringComparison.Ordinal))456{457throw new InvalidOperationException("기존 Asset GUID가 변경되었습니다: " + outputPaths[index]);458}459}460461var result = new MonsterMakerWriteResult(462definition,463updatedExisting,464outputPaths,465guidBefore,466guidAfter,467outputValidation);468transaction.Commit();469return result;470}471catch (Exception buildException)472{473try474{475transaction.Rollback();476}477catch (Exception rollbackException)478{479throw new AggregateException(480"Monster Maker 생성과 원상복구가 모두 실패했습니다.",481buildException,482rollbackException);483}484485throw;
부술 수 있는 성벽까지 경로 비용으로
설계와 실행 흐름 읽기 ↗공유 비용 지도의 키와 무효화
Assets/ProjectMT/04_Contents/01_CastleRaid/HexVariant/Runtime/HexCastleAssaultNavigation.cs · L209–255원본 파일 식별값 · SHA-256
310d306c32c3c94b6f69849aaa8e86ba60c647191b0368738b55c5c0522c4af6209public void Invalidate()210{211fields.Clear();212}213214public bool TryResolveRoute(215HexCoordinates start,216HexCastleAssaultRoutePolicy policy,217int expectedDefenseLayer,218float damagePerSecond,219float moveSpeed,220int topologyVersion,221out HexCastleAssaultRoutePlan plan)222{223plan = null;224if (!cells.ContainsKey(start) || palaceFootprint.Contains(start))225{226return false;227}228229var damageBand = Mathf.Max(1, Mathf.RoundToInt(Mathf.Max(1f, damagePerSecond) / 10f));230var speedBand = Mathf.Max(1, Mathf.RoundToInt(Mathf.Max(0.1f, moveSpeed) * 4f));231var key = new FieldKey(232policy,233Mathf.Max(0, expectedDefenseLayer),234damageBand,235speedBand,236topologyVersion);237if (!fields.TryGetValue(key, out var field))238{239field = BuildReverseField(240policy,241key.ExpectedDefenseLayer,242damageBand * 10f,243speedBand / 4f);244fields.Add(key, field);245}246247if (!field.ContainsKey(start))248{249return false;250}251252var path = ReconstructPath(253start,254field,255policy,
역방향 비용 지도 생성
Assets/ProjectMT/04_Contents/01_CastleRaid/HexVariant/Runtime/HexCastleAssaultNavigation.cs · L402–451원본 파일 식별값 · SHA-256
310d306c32c3c94b6f69849aaa8e86ba60c647191b0368738b55c5c0522c4af6402private Dictionary<HexCoordinates, float> BuildReverseField(403HexCastleAssaultRoutePolicy policy,404int expectedDefenseLayer,405float damagePerSecond,406float moveSpeed)407{408var result = new Dictionary<HexCoordinates, float>();409var heap = new MinimumHeap();410foreach (var goal in palaceApproaches)411{412if (!CanUseCell(goal, expectedDefenseLayer))413{414continue;415}416417result[goal] = 0f;418heap.Push(new QueueNode(goal, 0f));419}420421while (heap.Count > 0)422{423var current = heap.Pop();424if (!result.TryGetValue(current.Coordinates, out var currentCost) ||425current.Cost > currentCost + 0.0001f)426{427continue;428}429430var entryCost = ResolveEntryCost(431current.Coordinates,432policy,433expectedDefenseLayer,434damagePerSecond,435moveSpeed);436if (float.IsPositiveInfinity(entryCost))437{438continue;439}440441for (var direction = 0; direction < HexCoordinates.Directions.Length; direction++)442{443var predecessor = current.Coordinates.Neighbor(direction);444if (!CanUseCell(predecessor, expectedDefenseLayer) || palaceFootprint.Contains(predecessor))445{446continue;447}448449var candidate = currentCost + entryCost;450if (result.TryGetValue(predecessor, out var known) && candidate >= known - 0.0001f)451{
이동 시간과 파괴 비용
Assets/ProjectMT/04_Contents/01_CastleRaid/HexVariant/Runtime/HexCastleAssaultNavigation.cs · L540–561원본 파일 식별값 · SHA-256
310d306c32c3c94b6f69849aaa8e86ba60c647191b0368738b55c5c0522c4af6540private float ResolveEntryCost(541HexCoordinates coordinates,542HexCastleAssaultRoutePolicy policy,543int expectedDefenseLayer,544float damagePerSecond,545float moveSpeed)546{547if (!CanUseCell(coordinates, expectedDefenseLayer))548{549return float.PositiveInfinity;550}551552var travel = cellTravelDistance / Mathf.Max(0.1f, moveSpeed);553var cell = cells[coordinates];554if (!cell.IsBlocked)555{556return travel;557}558559var destruction = cell.CurrentHealth / Mathf.Max(1f, damagePerSecond);560return travel + destruction * ResolveDestructionWeight(cell, policy);561}
저장이 성공한 상태만 플레이에 확정
설계와 실행 흐름 읽기 ↗후보 저장 성공 후 현재 상태 확정
Assets/ProjectMT/02_Shared/GameData/GameDataService.cs · L93–142원본 파일 식별값 · SHA-256
289e29fff5da12fa5efa143b119a0157a9cb2bdaaf486e8e95d85d65409b28ad93private async Task<(bool Success, CommanderSkillSummonReceipt Receipt)> ApplyAndSaveCoreAsync(GameProgressChange change)94{95var notifyChanged = false;96CommanderSkillSummonReceipt receipt = null;97await gate.WaitAsync();98try99{100if (!IsLoaded)101{102ProjectMT.Shared.Audio.SfxProgressSounds.Notify(change, false);103return (false, null);104}105106var candidate = current.Clone(107commanderSkillBalanceConfig,108commanderSkillSummonConfig); // 원본 보존 후 변경 검증109if (!candidate.TryApply(110change,111commanderGrowthConfig,112itemCatalog,113equipmentBalanceConfig,114commanderSkillBalanceConfig,115commanderSkillSummonConfig))116{117ProjectMT.Shared.Audio.SfxProgressSounds.Notify(change, false);118return (false, null);119}120121try { await saveService.SaveAsync(candidate); } // 저장 성공을 먼저 확인122catch123{124try { ProjectMT.Shared.Audio.SfxEvents.Play2D(ProjectMT.Shared.Audio.SfxEvents.SaveError); }125catch (Exception soundError) { UnityEngine.Debug.LogException(soundError); }126throw;127}128current = candidate; // 성공한 후보만 확정129receipt = candidate.CommanderSkillSummonReceipt;130notifyChanged = !change.SuppressChangedNotification;131}132finally133{134gate.Release();135}136137ProjectMT.Shared.Audio.SfxProgressSounds.Notify(change, true);138if (notifyChanged)139{140Changed?.Invoke();141}142
정산 재진입 가드와 저장 결과 처리
Assets/ProjectMT/04_Contents/00_Framework/Runtime/ContentFlow.cs · L456–520원본 파일 식별값 · SHA-256
9e3e4e9ee67b4340e81e6da28c3f488a01ef52f9bdb831fc892b4b9a73ea0f26456private async Task TrySaveAndFinishAsync(ActiveRun run)457{458if (run == null || !ReferenceEquals(activeRun, run) || Phase != ContentFlowPhase.Finishing ||459run.PendingChange == null || Interlocked.Exchange(ref run.SettlementInFlight, 1) != 0)460{461return;462}463464run.CanRetry = false;465ShowSaving();466467var saved = false;468try469{470saved = await progress.TryApplyAndSaveAsync(run.PendingChange);471}472catch (Exception exception)473{474Debug.LogException(exception);475}476finally477{478Interlocked.Exchange(ref run.SettlementInFlight, 0);479}480481if (!ReferenceEquals(activeRun, run))482{483return;484}485486if (!saved)487{488Debug.LogError($"Content progress could not be saved. Content={run.Definition.ContentId}");489run.CanRetry = true;490ShowSaveFailed(() => RetrySave(run));491return;492}493494HideFinishFeedback();495PlayResultSound(run);496if (run.PendingResultPresentation != null && resultView != null)497{498try499{500await resultView.ShowAsync(run.PendingResultPresentation);501}502catch (Exception exception)503{504Debug.LogException(exception); // 결과 표현 실패가 복귀를 막지 않음505}506507if (!ReferenceEquals(activeRun, run))508{509return;510}511}512513if (run.PendingPresentation != null)514{515try516{517rewardPresentation?.PlayConfirmed(run.PendingPresentation); // 저장 성공 뒤에만 화면 연출 허용518}519catch (Exception exception)520{