using System; using System.Collections.Generic; using System.Globalization; using System.Text; // МОДУЛЬ АРМИИ: единственное место, где хранятся войска. Документ: Docs/Modules/Army.md // Без Unity: правила те же, что будут на сервере. // // Один солдат -- в одном месте: либо в городе (troops), либо в походе (marches). // Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам. // Гарнизон (garrison) -- часть воинов города: они в troops, но в поход не уходят. garrison[i] <= troops[i] всегда. // Очередь обучения (training) -- тоже здесь (решения по 8 вопросам Саши, 31.10, п. 8): заказы, сроки, завершение. // Раненые (решения 31.10, п. 7): ждут места (woundedWaiting) или лечатся (healing) -- не в troops и не в походах. // Числа госпиталя (регламент 31.10, ТЕСТ). В игре -- из таблицы HospitalConfig. public sealed class HospitalRules { public int TemporaryBeds = 5; // мест, пока здания госпиталя нет (решения 01.11, п. 3); после постройки -- штатные, не вместе public int[] HealGameHours = { 4, 4, 6, 8 }; // лечение одного воина: пехота и лучники 4, кавалерия 6, осадные 8 ИГРОВЫХ часов public long GameHourMs = 5 * 60 * 1000; // 1 игровой час = 5 реальных минут // Цена лечения одного воина по типам (решения 01.11, п. 3: 1 / 1 / 2 / 3 золота -- в таблице HospitalConfig); пусто -- без цены public ResourceAmount[][] HealCostPerSoldier = null; public static HospitalRules Default => new HospitalRules(); public long HealMs(int type) { int hours = HealGameHours != null && type >= 0 && type < HealGameHours.Length ? Math.Max(0, HealGameHours[type]) : 4; return hours * GameHourMs; } } // Пропавшие без вести (решения 01.11, п. 5). Доля возвращающихся и формула -- НЕ УТВЕРЖДЕНЫ (ТЕСТ, таблица HospitalConfig) public sealed class MissingRules { public int ReturnPercent = 50; // сколько пропавших вернётся (утверждён диапазон 10-90%, формула открыта) public int WoundedPercent = 30; // из вернувшихся -- раненые (утверждено: 70% здоровыми, 30% ранеными) public int WindowGameHours = 6; // окно возвращения после обратного пути, ИГРОВЫХ часов public long GameHourMs = 5 * 60 * 1000; public int MaxGroups = 4; // возвращаются 2-4 группами (меньше, если пропавших мало) public static MissingRules Default => new MissingRules(); } public sealed class ArmyService : IArmy, ISaveParticipant { public const int Types = 4; // пехота, лучники, кавалерия, осадные -- порядок как в GameData.Troops private class MarchUnits { public int marchId; public int[] units = new int[Types]; } private readonly int[] troops = new int[Types]; private readonly int[] garrison = new int[Types]; private readonly List marches = new List(); private class TrainingOrder { public int id, type, count; public long startMs, endMs; public bool recruited; // под заказ взяты жители (Население). false -- старый заказ, людей не брали } private class HealingBatch { public int id, type, count; public long startMs, endMs; } private readonly int[] woundedWaiting = new int[Types]; private readonly List healing = new List(); private readonly List healingView = new List(); private int nextHealId = 1; private readonly HospitalRules hospital; // Пропавшие без вести одного боя: группы возвращения рассчитаны сразу, при постановке (детерминированно) private class MissingEvent { public int id, marchId; public long windowStartMs, endMs; public int[] remaining = new int[Types]; // ещё не вернулись и не списаны public List groups = new List(); } private class MissingGroup { public int index; public long timeMs; public int[] healthy = new int[Types], wounded = new int[Types]; } private readonly List missing = new List(); private readonly List missingView = new List(); private readonly HashSet missingMarches = new HashSet(); // по каким походам пропавшие уже отмечены private int nextMissingId = 1; private readonly MissingRules missingRules; private readonly List training = new List(); private readonly List trainingView = new List(); private int nextOrderId = 1; // Секция "army" была в сохранении -- старые войска из progress.json переносить не нужно public bool RestoredFromSave { get; private set; } // ---------------- Контракт IArmy ---------------- public int TypeCount => Types; // Слоты похода: база из таблицы MarchConfig (регламент 31.10: 2, ТЕСТ). Бонус «Военной логистики» -- позже. private readonly int baseMarchSlots; public ArmyService(int baseMarchSlots = 2, HospitalRules hospital = null, MissingRules missingRules = null) { this.baseMarchSlots = Math.Max(1, baseMarchSlots); this.hospital = hospital ?? HospitalRules.Default; this.missingRules = missingRules ?? MissingRules.Default; } public HospitalRules Hospital => hospital; public int MarchSlots => baseMarchSlots; public int ActiveMarches => marches.Count; public int GetTroops(int type) { return type >= 0 && type < Types ? troops[type] : 0; } public int SoldiersAtHome { get { int n = 0; foreach (var t in troops) n += t; return n; } } public int TroopsInMarches { get { int n = 0; foreach (var m in marches) foreach (var u in m.units) n += u; return n; } } public int GarrisonTotal { get { int n = 0; foreach (var g in garrison) n += g; return n; } } public int GetGarrison(int type) { return type >= 0 && type < Types ? garrison[type] : 0; } public int AvailableForMarch => SoldiersAtHome - GarrisonTotal; // Гарнизон набирается по составу армии в городе: 30 пехоты и 10 лучников, гарнизон 8 -> 6 пехоты и 2 лучника public int SetGarrison(int count) { count = Math.Max(0, Math.Min(count, SoldiersAtHome)); int[] units = count > 0 ? ArmyMath.TakeProportional(troops, count) : null; for (int i = 0; i < Types; i++) garrison[i] = units != null ? units[i] : 0; Changed(); return count; } public void AddTroops(int type, int count) { if (type < 0 || type >= Types || count <= 0) return; troops[type] += count; Changed(); } public int LoseFraction(double fraction) { if (fraction <= 0) return 0; if (fraction > 1) fraction = 1; int lost = 0; for (int i = 0; i < Types; i++) { int l = (int)Math.Floor(troops[i] * fraction); troops[i] -= l; lost += l; } ClampGarrison(); if (lost > 0) Changed(); return lost; } public bool TakeForMarch(int marchId, int count) { // Поход с таким номером уже есть: перезапись потеряла бы его воинов if (marches.Exists(m => m.marchId == marchId)) return false; // Все слоты похода заняты. Походы из старых сохранений сверх лимита дойдут, но новых не будет if (marches.Count >= MarchSlots) return false; // В поход -- только свободные воины, гарнизон остаётся в городе var free = new int[Types]; for (int i = 0; i < Types; i++) free[i] = troops[i] - garrison[i]; int[] taken = ArmyMath.TakeProportional(free, count); if (taken == null) return false; for (int i = 0; i < Types; i++) troops[i] -= taken[i]; marches.Add(new MarchUnits { marchId = marchId, units = taken }); Changed(); return true; } // Воины возвращаются только из известного похода, и только один раз: запись похода удаляется. // Неизвестный поход (повторное событие, испорченное сохранение) не добавляет никого -- иначе солдаты // появлялись бы из ниоткуда. Перенос старых сохранений -- отдельно, в ImportLegacy. public bool ReturnFromMarch(int marchId, int survivors) { MarchUnits entry = marches.Find(m => m.marchId == marchId); if (entry == null) return false; int sent = 0; foreach (var u in entry.units) sent += u; int[] back = ArmyMath.SplitSurvivors(entry.units, Math.Min(survivors, sent)); // больше, чем ушло, не вернётся for (int i = 0; i < Types; i++) troops[i] += back[i]; marches.Remove(entry); Changed(); return true; } public int[] GetMarchUnits(int marchId) { MarchUnits entry = marches.Find(m => m.marchId == marchId); return entry != null ? (int[])entry.units.Clone() : null; } // Выжившие точно по типам: бой посчитал потери каждого типа. Больше, чем ушло, вернуться не может. // Правила те же, что у ReturnFromMarch: только известный поход, только один раз. public bool ReturnUnits(int marchId, int[] survivorUnits) { return ReturnUnits(marchId, survivorUnits, null, 0); } // Выжившие -- в город, раненые -- в госпиталь (решения 31.10, п. 7). Остальные из ушедших погибли. // Один солдат в одном месте: выжившие + раненые не больше, чем ушло, по каждому типу. public bool ReturnUnits(int marchId, int[] survivorUnits, int[] woundedUnits, long nowMs) { MarchUnits entry = marches.Find(m => m.marchId == marchId); if (entry == null) return false; bool anyWounded = false; if (woundedUnits != null) ProcessHealing(nowMs); // сначала освободить места, которые освободились раньше for (int i = 0; i < Types; i++) { int alive = survivorUnits != null && i < survivorUnits.Length ? Math.Max(0, Math.Min(survivorUnits[i], entry.units[i])) : 0; int hurt = woundedUnits != null && i < woundedUnits.Length ? Math.Max(0, Math.Min(woundedUnits[i], entry.units[i] - alive)) : 0; troops[i] += alive; woundedWaiting[i] += hurt; if (hurt > 0) anyWounded = true; } marches.Remove(entry); if (anyWounded) FillBeds(nowMs); Changed(); return true; } // ---------------- Раненые и лечение ---------------- public int WoundedTotal { get { int n = 0; foreach (var w in woundedWaiting) n += w; foreach (var b in healing) n += b.count; return n; } } public int GetAllTroops(int type) { if (type < 0 || type >= Types) return 0; int n = troops[type] + woundedWaiting[type]; foreach (var m in marches) n += m.units[type]; foreach (var b in healing) if (b.type == type) n += b.count; return n; } // ---------------- Пропавшие без вести ---------------- public int MissingTotal { get { int n = 0; foreach (var e in missing) foreach (var r in e.remaining) n += r; return n; } } public IReadOnlyList Missing { get { missingView.Clear(); foreach (var e in missing) { int n = 0; foreach (var r in e.remaining) n += r; missingView.Add(new MissingInfo { Id = e.id, Count = n, WindowStartMs = e.windowStartMs, EndMs = e.endMs }); } return missingView; } } public bool MarkMissing(int marchId, int[] missingUnits, long battleMs, long travelMs) { if (missingUnits == null || missingMarches.Contains(marchId)) return false; MarchUnits entry = marches.Find(m => m.marchId == marchId); if (entry == null) return false; var e = new MissingEvent { id = nextMissingId++, marchId = marchId }; int total = 0; for (int i = 0; i < Types && i < missingUnits.Length; i++) { int n = Math.Max(0, Math.Min(missingUnits[i], entry.units[i])); entry.units[i] -= n; // из похода -- в пропавшие: один солдат в одном месте e.remaining[i] = n; total += n; } if (total == 0) { nextMissingId--; return false; } e.windowStartMs = battleMs + Math.Max(0, travelMs); e.endMs = e.windowStartMs + Math.Max(0, missingRules.WindowGameHours) * missingRules.GameHourMs; PlanGroups(e, battleMs); missing.Add(e); missingMarches.Add(marchId); Changed(); return true; } // Кто вернётся и когда: доля возвращающихся по типам (дробная часть -- бросок), 2-4 группы в окне, // в каждой -- 70% здоровых, 30% раненых (дробная часть -- бросок). Тип войск сохраняется. void PlanGroups(MissingEvent e, long seedTime) { ulong seed = (ulong)e.marchId * 0x9E3779B97F4A7C15UL ^ (ulong)seedTime; int roll = 0; bool Chance(long num, long den) => den > 0 && (long)(Next(seed, roll++) % (ulong)den) < num; var back = new int[Types]; int returning = 0; for (int i = 0; i < Types; i++) { long num = (long)e.remaining[i] * Math.Max(0, Math.Min(100, missingRules.ReturnPercent)); back[i] = (int)(num / 100) + (Chance(num % 100, 100) ? 1 : 0); returning += back[i]; } if (returning == 0) return; int groups = Math.Max(1, Math.Min(Math.Max(2, missingRules.MaxGroups), returning)); // мало вернувшихся -- групп меньше if (returning >= 2) groups = Math.Max(2, Math.Min(groups, 2 + (int)(Next(seed, roll++) % (ulong)(Math.Max(2, missingRules.MaxGroups) - 1)))); long window = Math.Max(1, e.endMs - e.windowStartMs); for (int g = 0; g < groups; g++) e.groups.Add(new MissingGroup { index = g + 1, timeMs = e.windowStartMs + window * (g + 1) / (groups + 1) }); // Раздаём вернувшихся по группам по очереди, внутри группы -- здоровые и раненые for (int i = 0; i < Types; i++) for (int k = 0; k < back[i]; k++) { var grp = e.groups[(k + i) % groups]; if (Chance(Math.Max(0, Math.Min(100, missingRules.WoundedPercent)), 100)) grp.wounded[i]++; else grp.healthy[i]++; } } static ulong Next(ulong seed, int n) { ulong x = seed + 0x9E3779B97F4A7C15UL * (ulong)(n + 1); x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL; x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL; return x ^ (x >> 31); } public int ProcessMissing(long nowMs) { int done = 0; bool changed = false; for (int k = missing.Count - 1; k >= 0; k--) { var e = missing[k]; e.groups.Sort((a, b) => a.timeMs.CompareTo(b.timeMs)); while (e.groups.Count > 0 && e.groups[0].timeMs <= nowMs && e.groups[0].timeMs <= e.endMs) { var g = e.groups[0]; e.groups.RemoveAt(0); // группа возвращается один раз int healthy = 0, wounded = 0; bool anyWounded = false; for (int i = 0; i < Types; i++) { int h = Math.Min(g.healthy[i], e.remaining[i]); int w = Math.Min(g.wounded[i], e.remaining[i] - h); e.remaining[i] -= h + w; troops[i] += h; // исходной армии может уже не быть -- в резерв поселения woundedWaiting[i] += w; // раненые -- в госпиталь по обычным правилам healthy += h; wounded += w; if (w > 0) anyWounded = true; } if (anyWounded) { ProcessHealing(g.timeMs); FillBeds(g.timeMs); } done++; changed = true; EventBus.Publish(new MissingReturnedEvent { MissingId = e.id, Group = g.index, Healthy = healthy, Wounded = wounded, TimeMs = g.timeMs }); } if (nowMs >= e.endMs) { int left = 0; foreach (var r in e.remaining) left += r; missing.RemoveAt(k); // окно кончилось: кто не вернулся -- списан, места освобождены changed = true; EventBus.Publish(new MissingWrittenOffEvent { MissingId = e.id, Count = left, TimeMs = e.endMs }); } } if (changed) Changed(); return done; } public int GetWoundedWaiting(int type) { return type >= 0 && type < Types ? woundedWaiting[type] : 0; } public IReadOnlyList Healing { get { healingView.Clear(); foreach (var b in healing) healingView.Add(new HealingInfo { Id = b.id, Type = b.type, Count = b.count, StartMs = b.startMs, EndMs = b.endMs }); return healingView; } } // Места: от здания госпиталя (Поселение), пока его нет -- временные из таблицы public int HospitalBeds => GameServices.TryGet(out IHospital h) && h.Beds > 0 ? h.Beds : Math.Max(0, hospital.TemporaryBeds); public int ProcessHealing(long nowMs) { int done = 0; // По времени: самая ранняя группа заканчивает -- на её место в тот же момент ложатся ждущие while (true) { HealingBatch next = null; foreach (var b in healing) if (b.endMs <= nowMs && (next == null || b.endMs < next.endMs || b.endMs == next.endMs && b.id < next.id)) next = b; if (next == null) break; healing.Remove(next); // группа вылечена один раз: повторный расчёт её уже не найдёт troops[next.type] += next.count; done++; EventBus.Publish(new SoldiersHealedEvent { BatchId = next.id, Type = next.type, Count = next.count, TimeMs = next.endMs }); FillBeds(next.endMs); } FillBeds(nowMs); // мест могло стать больше (госпиталь улучшили) if (done > 0) Changed(); return done; } // Положить ждущих на свободные места в момент atMs: по типам по порядку, каждая группа -- свой срок лечения. // Цена лечения (если задана) списывается через Экономику целиком; не хватает -- группа ждёт. void FillBeds(long atMs) { int free = HospitalBeds; foreach (var b in healing) free -= b.count; for (int type = 0; type < Types && free > 0; type++) { int n = Math.Min(free, woundedWaiting[type]); if (n <= 0) continue; if (!PayForHealing(type, n)) continue; woundedWaiting[type] -= n; free -= n; healing.Add(new HealingBatch { id = nextHealId++, type = type, count = n, startMs = atMs, endMs = atMs + hospital.HealMs(type) }); } } bool PayForHealing(int type, int soldiers) { var perSoldier = hospital.HealCostPerSoldier != null && type < hospital.HealCostPerSoldier.Length ? hospital.HealCostPerSoldier[type] : null; if (perSoldier == null || perSoldier.Length == 0) return true; // цена не утверждена -- без цены if (!GameServices.TryGet(out IEconomy economy)) return false; var cost = new List(); foreach (var c in perSoldier) if (c.Amount > 0) cost.Add(new ResourceAmount(c.Type, c.Amount * soldiers)); return cost.Count == 0 || economy.TrySpend(cost); } // ---------------- Очередь обучения ---------------- public IReadOnlyList Training { get { trainingView.Clear(); foreach (var o in training) trainingView.Add(new TrainingOrderInfo { Id = o.id, Type = o.type, Count = o.count, StartMs = o.startMs, EndMs = o.endMs }); return trainingView; } } public bool CanQueueTraining(int type, int count, int queueSize, out string error) { error = null; if (type < 0 || type >= Types || count <= 0) error = "Неверный заказ"; else if (training.Count >= queueSize) error = "Очередь обучения заполнена. Больше казарм -- длиннее очередь"; else if (GameServices.TryGet(out IPopulation population) && population.Recruitable < count) error = population.Recruitable > 0 ? $"Жителей хватит только на {population.Recruitable} воинов" : $"Некого нанять: свободных жителей нет (резерв {population.CivilianReserve}). Снимите рабочих или стройте Дома"; return error == null; } public bool TryQueueTraining(int type, int count, long durationMs, int queueSize, long nowMs, out string error) { if (!CanQueueTraining(type, count, queueSize, out error)) return false; // Воин -- житель города: люди уходят в Казармы сейчас, в Армию -- когда обучатся bool recruited = false; if (GameServices.TryGet(out IPopulation population)) { if (!population.TryRecruit(count)) { error = "Некого нанять: жителей не хватает"; return false; } recruited = true; } // Заказы идут друг за другом: новый начинается, когда закончится последний long start = training.Count > 0 ? Math.Max(nowMs, training[training.Count - 1].endMs) : nowMs; training.Add(new TrainingOrder { id = nextOrderId++, type = type, count = count, startMs = start, endMs = start + Math.Max(0, durationMs), recruited = recruited }); Changed(); return true; } public int ProcessTraining(long nowMs) { int done = 0; while (training.Count > 0 && training[0].endMs <= nowMs) { TrainingOrder o = training[0]; if (o.recruited) { // Новобранцы переходят из Населения в Армию одним действием (Население само зовёт AddTroops). // Населения ещё нет (запуск) -- подождём: заказ остаётся, никто не теряется. if (!GameServices.TryGet(out IPopulation population)) break; if (!population.CompleteTraining(o.type, o.count)) { // Население не нашло этих новобранцев (испорченное сохранение): заказ снимается без воинов -- // иначе люди появились бы из ниоткуда, а очередь встала бы навсегда training.RemoveAt(0); Changed(); continue; } } else AddTroops(o.type, o.count); // старый заказ: людей под него не брали training.RemoveAt(0); // заказ закончен один раз: повторный расчёт его уже не найдёт done++; EventBus.Publish(new TroopsTrainedEvent { OrderId = o.id, Type = o.type, Count = o.count, TimeMs = o.endMs }); } return done; } public int FinishAllTraining(long nowMs) { foreach (var o in training) { o.endMs = Math.Min(o.endMs, nowMs); o.startMs = Math.Min(o.startMs, o.endMs); } return ProcessTraining(nowMs); } // Перенос очереди из старого progress.json (её вёл GameProgress). Время уже переведено в мс часов игры. public void ImportLegacyTraining(IEnumerable<(int type, int count, long startMs, long endMs, bool recruited)> orders) { if (orders == null) return; foreach (var o in orders) { if (o.type < 0 || o.type >= Types || o.count <= 0) continue; training.Add(new TrainingOrder { id = nextOrderId++, type = o.type, count = o.count, startMs = o.startMs, endMs = Math.Max(o.startMs, o.endMs), recruited = o.recruited }); } Changed(); } void ClampGarrison() { for (int i = 0; i < Types; i++) garrison[i] = Math.Max(0, Math.Min(garrison[i], troops[i])); } void Changed() { EventBus.Publish(new TroopsChangedEvent { AtHome = SoldiersAtHome, InMarches = TroopsInMarches, Garrison = GarrisonTotal, Wounded = WoundedTotal, Missing = MissingTotal }); } // ---------------- Перенос из старого progress.json ---------------- // Войска раньше хранил GameProgress (troops) и мост ArmyBridge (sashaMarchUnits). // Вызывается один раз, если в save.json ещё нет секции "army". public void ImportLegacy(int[] legacyTroops, IEnumerable> legacyMarches) { if (legacyTroops != null) for (int i = 0; i < Types && i < legacyTroops.Length; i++) troops[i] = Math.Max(0, legacyTroops[i]); Array.Clear(garrison, 0, Types); marches.Clear(); if (legacyMarches != null) { foreach (var m in legacyMarches) { if (marches.Exists(x => x.marchId == m.Key)) continue; // один поход -- одна запись var units = new int[Types]; for (int i = 0; i < Types && m.Value != null && i < m.Value.Length; i++) units[i] = Math.Max(0, m.Value[i]); marches.Add(new MarchUnits { marchId = m.Key, units = units }); } } Changed(); } // ---------------- Сохранение (секция "army") ---------------- // Модуль без Unity, поэтому формат -- простые строки: // troops=<пехота>,<лучники>,<кавалерия>,<осадные> // garrison=<пехота>,<лучники>,<кавалерия>,<осадные> (из troops; нет строки -- гарнизона нет) // march=:<пехота>,<лучники>,<кавалерия>,<осадные> // train=|<тип>|<сколько>|<начало, мс>|<конец, мс>|<люди взяты 1/0> (по порядку очереди) // trainNext=<номер следующего заказа> // wounded=<пехота>,<лучники>,<кавалерия>,<осадные> (ждут места в госпитале) // heal=|<тип>|<сколько>|<начало, мс>|<конец, мс> // healNext=<номер следующей группы лечения> // missing=|<поход>|<окно с, мс>|<конец, мс>|<осталось по типам через ,> // mgroup=|<номер группы>|<время, мс>|<здоровые по типам>|<раненые по типам> (ещё не вернувшиеся группы) // missingMarch=<поход> (по нему пропавшие уже отмечены) missingNext=<номер> public string SaveKey => "army"; public string Capture() { var sb = new StringBuilder(); sb.Append("troops=").Append(Join(troops)).Append('\n'); sb.Append("garrison=").Append(Join(garrison)).Append('\n'); foreach (var m in marches) sb.Append("march=").Append(m.marchId.ToString(CultureInfo.InvariantCulture)).Append(':').Append(Join(m.units)).Append('\n'); foreach (var o in training) sb.Append("train=").Append(string.Join("|", new[] { N(o.id), N(o.type), N(o.count), o.startMs.ToString(CultureInfo.InvariantCulture), o.endMs.ToString(CultureInfo.InvariantCulture), o.recruited ? "1" : "0" })).Append('\n'); sb.Append("trainNext=").Append(N(nextOrderId)).Append('\n'); sb.Append("wounded=").Append(Join(woundedWaiting)).Append('\n'); foreach (var b in healing) sb.Append("heal=").Append(string.Join("|", new[] { N(b.id), N(b.type), N(b.count), b.startMs.ToString(CultureInfo.InvariantCulture), b.endMs.ToString(CultureInfo.InvariantCulture) })).Append('\n'); sb.Append("healNext=").Append(N(nextHealId)).Append('\n'); foreach (var e in missing) { sb.Append("missing=").Append(string.Join("|", new[] { N(e.id), N(e.marchId), e.windowStartMs.ToString(CultureInfo.InvariantCulture), e.endMs.ToString(CultureInfo.InvariantCulture), Join(e.remaining) })).Append('\n'); foreach (var g in e.groups) sb.Append("mgroup=").Append(string.Join("|", new[] { N(e.id), N(g.index), g.timeMs.ToString(CultureInfo.InvariantCulture), Join(g.healthy), Join(g.wounded) })).Append('\n'); } foreach (var id in missingMarches) sb.Append("missingMarch=").Append(N(id)).Append('\n'); sb.Append("missingNext=").Append(N(nextMissingId)).Append('\n'); return sb.ToString(); } public void Restore(string data) { Array.Clear(troops, 0, Types); Array.Clear(garrison, 0, Types); marches.Clear(); training.Clear(); nextOrderId = 1; Array.Clear(woundedWaiting, 0, Types); healing.Clear(); nextHealId = 1; missing.Clear(); missingMarches.Clear(); nextMissingId = 1; RestoredFromSave = true; if (!string.IsNullOrEmpty(data)) { foreach (var raw in data.Split('\n')) { string line = raw.Trim(); if (line.StartsWith("troops=")) { int[] t = Parse(line.Substring("troops=".Length)); Array.Copy(t, troops, Types); } else if (line.StartsWith("garrison=")) { int[] g = Parse(line.Substring("garrison=".Length)); Array.Copy(g, garrison, Types); } else if (line.StartsWith("march=")) { string[] parts = line.Substring("march=".Length).Split(':'); if (parts.Length == 2 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int id) && !marches.Exists(x => x.marchId == id)) // один поход -- одна запись marches.Add(new MarchUnits { marchId = id, units = Parse(parts[1]) }); } else if (line.StartsWith("train=")) { string[] p = line.Substring("train=".Length).Split('|'); if (p.Length == 6 && int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int oid) && int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int type) && type >= 0 && type < Types && int.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int count) && count > 0 && long.TryParse(p[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out long start) && long.TryParse(p[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out long end) && !training.Exists(x => x.id == oid)) // один заказ -- одна запись { training.Add(new TrainingOrder { id = oid, type = type, count = count, startMs = start, endMs = end, recruited = p[5] == "1" }); nextOrderId = Math.Max(nextOrderId, oid + 1); } } else if (line.StartsWith("wounded=")) { int[] w = Parse(line.Substring("wounded=".Length)); Array.Copy(w, woundedWaiting, Types); } else if (line.StartsWith("heal=")) { string[] p = line.Substring("heal=".Length).Split('|'); if (p.Length == 5 && int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int hid) && int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int htype) && htype >= 0 && htype < Types && int.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int hcount) && hcount > 0 && long.TryParse(p[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out long hstart) && long.TryParse(p[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out long hend) && !healing.Exists(x => x.id == hid)) { healing.Add(new HealingBatch { id = hid, type = htype, count = hcount, startMs = hstart, endMs = hend }); nextHealId = Math.Max(nextHealId, hid + 1); } } else if (line.StartsWith("missing=")) { string[] p = line.Substring("missing=".Length).Split('|'); if (p.Length == 5 && int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int mid) && int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int mmarch) && long.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out long mstart) && long.TryParse(p[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out long mend) && !missing.Exists(x => x.id == mid)) { missing.Add(new MissingEvent { id = mid, marchId = mmarch, windowStartMs = mstart, endMs = mend, remaining = Parse(p[4]) }); nextMissingId = Math.Max(nextMissingId, mid + 1); } } else if (line.StartsWith("mgroup=")) { string[] p = line.Substring("mgroup=".Length).Split('|'); MissingEvent owner = null; if (p.Length == 5 && int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int gid)) owner = missing.Find(x => x.id == gid); // строки missing= идут раньше своих групп if (owner != null && int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int gidx) && long.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out long gtime) && !owner.groups.Exists(x => x.index == gidx)) owner.groups.Add(new MissingGroup { index = gidx, timeMs = gtime, healthy = Parse(p[3]), wounded = Parse(p[4]) }); } else if (line.StartsWith("missingMarch=") && int.TryParse(line.Substring("missingMarch=".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out int mm)) missingMarches.Add(mm); else if (line.StartsWith("missingNext=") && int.TryParse(line.Substring("missingNext=".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out int mnext)) nextMissingId = Math.Max(nextMissingId, mnext); else if (line.StartsWith("healNext=") && int.TryParse(line.Substring("healNext=".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out int hnext)) nextHealId = Math.Max(nextHealId, hnext); else if (line.StartsWith("trainNext=") && int.TryParse(line.Substring("trainNext=".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out int next)) nextOrderId = Math.Max(nextOrderId, next); } } ClampGarrison(); // гарнизон не больше войск, даже если файл правили руками Changed(); } // Новая игра: войск нет, гарнизона и походов нет public void ResetState() { Array.Clear(troops, 0, Types); Array.Clear(garrison, 0, Types); marches.Clear(); training.Clear(); nextOrderId = 1; Array.Clear(woundedWaiting, 0, Types); healing.Clear(); nextHealId = 1; missing.Clear(); missingMarches.Clear(); nextMissingId = 1; Changed(); } static string N(int v) => v.ToString(CultureInfo.InvariantCulture); static string Join(int[] values) { var parts = new string[values.Length]; for (int i = 0; i < values.Length; i++) parts[i] = values[i].ToString(CultureInfo.InvariantCulture); return string.Join(",", parts); } static int[] Parse(string csv) { var result = new int[Types]; string[] parts = csv.Split(','); for (int i = 0; i < Types && i < parts.Length; i++) { if (int.TryParse(parts[i].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) result[i] = Math.Max(0, v); } return result; } }