VSClone/Assets/Editor/HeadlessRun.cs

192 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using UnityEditor;
using UnityEngine;
using VS.Sim;
namespace VS.EditorTools
{
// 배치 검증: Unity.exe -batchmode -projectPath <VSClone> -executeMethod VS.EditorTools.HeadlessRun.Run -logFile <log> -quit
// 결과 줄 = "[VS] PASS|FAIL <이름> …" · 마지막 "SUMMARY pass=N fail=M"
public static partial class HeadlessRun
{
static int pass, fail;
static readonly List<string> fails = new List<string>();
public static Tables LoadTables() => Tables.Load(n =>
{
foreach (var ext in new[] { ".json", ".csv" })
{
var p = Path.Combine("Assets/Resources/Data", n + ext);
if (File.Exists(p)) return File.ReadAllText(p);
}
return null;
});
public class Clock { public DateTime Base = new DateTime(2026, 1, 1, 9, 0, 0); public double Extra; public Game G; public DateTime Now() => Base.AddSeconds((G != null ? G.Time : 0) + Extra); }
public static Game NewGame(Tables t, out Clock clock, SaveData s = null)
{
clock = new Clock();
var c = clock;
var g = new Game(t, s ?? new SaveData(), () => c.Now());
c.G = g;
return g;
}
static void Check(string name, bool ok, string detail = "")
{
if (ok) pass++; else { fail++; fails.Add(name + " " + detail); }
Debug.Log($"[VS] {(ok ? "PASS" : "FAIL")} {name} {detail}");
}
static bool Near(decimal a, decimal b, decimal tol) => Math.Abs(a - b) <= tol;
static bool Near(double a, double b, double tol) => Math.Abs(a - b) <= tol;
public static void Run()
{
int exit = 0;
try
{
Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);
pass = fail = 0; fails.Clear();
var t = LoadTables();
CoreTests(t);
SystemTests(t);
BattleTests(t);
foreach (var f in fails) Debug.Log("[VS] FAILED: " + f);
Debug.Log($"[VS] SUMMARY pass={pass} fail={fail}");
if (fail > 0) exit = 1;
}
catch (Exception e) { Debug.LogError("[VS] EXCEPTION " + e); exit = 2; }
EditorApplication.Exit(exit);
}
static void CoreTests(Tables t)
{
// T1 데이터
Check("T1.rooms", t.Rooms.Length == 6 && t.Rooms[0].Items.Count == 11 && t.Rooms[5].Items[10].Levels.Count == 75);
Check("T1.skills", t.Skills.Items.Count == 5 && t.Skills.Items[0].Levels.Count == 451);
Check("T1.clothes_rank_quest", t.Clothes.Levels.Count == 72 && t.Ranks.Items.Count == 15 && t.Quests.items.Count == 267);
Check("T1.config", Math.Abs(t.Cfg.remote.BaseIncome - 0.007f) < 1e-6f && Math.Abs(t.Cfg.remote.TapMultiplier - 1.5f) < 1e-6f && t.Cfg.input._tapTimeWindow == 1f);
Check("T1.loc", t.Loc.Get("Content.Room.House1.0.Name") == "소파", t.Loc.Get("Content.Room.House1.0.Name"));
// T2 새 게임 (NewRoom RVA 0x251CF00)
var g = NewGame(t, out var clk);
Check("T2.newroom", g.S.rooms.Count == 1 && g.S.rooms[0].Type == 10 && g.S.rooms[0].Level == 9, $"lv={g.S.rooms[0].Level}");
Check("T2.follower", g.Follower == t.RoomFollower[0][10][0], g.Follower.ToString());
// T3 생산 수입 식 (RVA 0x255C74C)
Check("T3.income1", Math.Abs(g.ItemProductIncome(1) - 0.007f) < 1e-7f);
float e10 = 0.007f + 0.007f * 9 * (float)Math.Pow(1.2, 10 / 25.0);
Check("T3.income10", Math.Abs(g.ItemProductIncome(10) - e10) < 1e-6f, g.ItemProductIncome(10).ToString("R"));
Check("T3.room_income", g.CurrentRoomProductIncome() == (decimal)g.ItemProductIncome(10));
// T4 배율 (RVA 0x255B5A4 / 0x255BE70)
var lv9 = t.Rooms[0].Items[10].Levels[9];
Check("T4.passrate", Math.Abs(g.TotalPassRate() - (lv9.PassRate / 10000f + 1f)) < 1e-6f, g.TotalPassRate().ToString("R"));
Check("T4.taprate", Math.Abs(g.TotalTapRate() - (lv9.TapRate / 10000f + 1f)) < 1e-6f);
Check("T4.boost", g.GlobalIncomeBoost(true) == 1m * t.Ranks.Items[0].RankBoostRate);
decimal expPass = (decimal)g.ItemProductIncome(10) * (decimal)g.TotalPassRate() * 1m * 1m;
Check("T4.passgps", Near(g.PassGps(), expPass, 0.0000001m), g.PassGps().ToString());
decimal expTap = (decimal)g.ItemProductIncome(10) * (decimal)g.TotalTapRate() * (decimal)1.5f;
Check("T4.tapgps", Near(g.TapGps(), expTap, 0.0000001m), g.TapGps().ToString());
// T5 탭 → 후원 적립 · 1 초 1회 제한 (RVA 0x240588C · 0x23FECD0)
decimal tg = g.TapGps();
bool a1 = g.Tap();
g.Tick(0.3f);
bool a2 = g.Tap();
Check("T5.ratelimit", a1 && !a2);
Check("T5.donate", g.CoinDonate == decimal.Ceiling(tg * 100m) / 100m, g.CoinDonate.ToString());
g.Tick(1.0f);
Check("T5.next_window", g.Tap());
// T6 후원 받기 (RVA 0x2436500)
var g6 = NewGame(t, out _);
g6.CoinDonate = 12.75m;
var got = g6.ClaimDonate(1);
Check("T6.claim", got == 12 && g6.Coin == 12 && g6.CoinDonate == 0.75m, $"{got} {g6.Coin} {g6.CoinDonate}");
g6.CoinDonate = 5m;
Check("T6.claim_x2", g6.ClaimDonate(2) == 10 && g6.Coin == 22);
// T7 방 아이템 구매 (RVA 0x2431148)
var g7 = NewGame(t, out _);
g7.Coin = 100;
var f0 = g7.Follower;
var r7 = g7.BuyRoomItem(0);
Check("T7.buy", r7 == BuyResult.Ok && g7.Coin == 100 - t.RoomPrice[0][0][0] && g7.RoomLevel(0) == 0, r7.ToString());
Check("T7.follower", g7.Follower == f0 + t.RoomFollower[0][0][0]);
Check("T7.dependency_locked", !g7.RoomUnlocked(5) && g7.BuyRoomItem(5) == BuyResult.Locked);
g7.Coin = 0;
Check("T7.nomoney", g7.BuyRoomItem(1) == BuyResult.NoMoney);
// T8 방치 수입 1 초 단위 (RVA 0x240092C)
var g8 = NewGame(t, out _);
g8.Coin = 0;
decimal pg = g8.PassGps();
for (int i = 0; i < 100; i++) g8.Tick(0.1f);
decimal exp8 = decimal.Truncate(pg * 10m);
Check("T8.idle", (decimal)g8.Coin >= exp8 - 1m && (decimal)g8.Coin <= exp8 + 1m, $"coin={g8.Coin} exp~{pg * 10m}");
// T9 하이프 (RVA 0x2405E94 · 0x2407440)
var g9 = NewGame(t, out _);
decimal pBefore = g9.PassGps();
for (int i = 0; i < 60 && !g9.HypePlaying; i++) { g9.Tap(); g9.Tick(0.2f); }
Check("T9.hype_start", g9.HypePlaying && g9.AutoMultiplier == t.Cfg.remote.HypeMulti, $"hype={g9.Hype} playing={g9.HypePlaying}");
Check("T9.hype_x2", Near(g9.PassGps(), pBefore * (decimal)t.Cfg.remote.HypeMulti, 0.000001m));
for (int i = 0; i < 100; i++) g9.Tick(0.1f);
Check("T9.hype_end", !g9.HypePlaying && g9.AutoMultiplier == 1f, $"hype={g9.Hype}");
// T10 랭크 (RVA 0x25E3450)
var g10 = NewGame(t, out _);
g10.Follower = t.RankFollower[1];
Check("T10.pending", g10.RankUpPending);
var rw = g10.ApplyRankUp();
Check("T10.reward", rw == t.RankReward[1] && g10.S.rankIndex == 2 && g10.Coin == t.RankReward[1], rw.ToString());
g10.Follower = t.RankFollower[4];
var rw2 = g10.ApplyRankUp();
Check("T10.multi", rw2 == t.RankReward[2] + t.RankReward[3] + t.RankReward[4] && g10.S.rankIndex == 5);
// T11 스킬 (RVA 0x241AE98 · 0x255D25C)
var g11 = NewGame(t, out var c11);
g11.Coin = 1000;
Check("T11.buy", g11.BuySkill(0) == BuyResult.Ok && g11.SkillLevel(0) == 1 && g11.EffSkillIndex(0) == 0);
Check("T11.rate", Math.Abs(g11.TotalSkillPassRate() - t.Skills.Items[0].Levels[0].PassRate) < 0.01f);
// 조건 레벨: 해당 레벨 Condition > 현재 집이면 구매 불가
int condLv = t.Skills.Items[0].Levels.FindIndex(l => l.Condition > 0);
var g11b = NewGame(t, out _);
g11b.S.skills.Add(new SaveItem { Type = 0, Level = condLv });
Check("T11.condition", condLv > 0 && !g11b.CanBuySkill(0), $"condLv={condLv}");
// T12 업그레이드 타이머 → 유효 레벨 (RVA 0x255C658)
var g12 = NewGame(t, out var c12);
int timedLv = t.Rooms[0].Items[0].Levels.FindIndex(l => l.UpgradeTime > 0f);
g12.S.rooms.Add(new SaveItem { Type = 0, Level = timedLv - 1 });
g12.Coin = BigInteger.Parse("1000000000");
var r12 = g12.BuyRoomItem(0);
Check("T12.timer", r12 == BuyResult.Ok && g12.RoomUpgrading(0) && g12.EffRoomLevel(0) == timedLv - 1, $"lv={timedLv} r={r12}");
c12.Extra += t.Rooms[0].Items[0].Levels[timedLv].UpgradeTime + 1;
Check("T12.timer_done", !g12.RoomUpgrading(0) && g12.EffRoomLevel(0) == timedLv);
// T13 새 집 (RVA 0x255E530 · 0x251C780)
var g13 = NewGame(t, out _);
g13.S.rooms.Clear();
foreach (var it in t.Rooms[0].Items) g13.S.rooms.Add(new SaveItem { Type = it.Index, Level = it.Levels.Count - 1 });
Check("T13.allmax", g13.AllRoomItemsMaxed());
g13.RoomCompletionEconomy(0, out var inc, out var ps, out var tp);
bool moved = g13.MoveToNextHouse();
Check("T13.move", moved && g13.S.house == 1 && g13.TotalCompletedIncome == inc && Math.Abs(g13.S.sumPassRoom - ps) < 0.01f && g13.S.rooms.Count == 1 && g13.S.rooms[0].Level == 0);
Check("T13.carry", g13.TotalProductIncome() == inc + (decimal)g13.ItemProductIncome(1));
// T14 의상 레벨 배율 (RVA 0x255CA14)
var g14 = NewGame(t, out _);
Check("T14.stat0", g14.ClosetStatRate(0) == 10000);
g14.S.closetLevel = 1;
Check("T14.stat1", g14.ClosetStatRate(g14.ClosetEffLevel()) == t.Clothes.Levels[0].StatRate && g14.GlobalIncomeBoost(false) == (decimal)t.Clothes.Levels[0].StatRate / 10000m);
}
}
}