From e26eeaffafbb679fa1a20d5285b468d5b75c1900 Mon Sep 17 00:00:00 2001 From: Valdemar Date: Thu, 5 Nov 2026 15:12:19 +0300 Subject: [PATCH] =?UTF-8?q?3D:=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B5,=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BB=D0=B0=D0=B4=D0=BE=D1=87=D0=BD=D0=B0=D1=8F=20=D1=81?= =?UTF-8?q?=D0=B5=D1=82=D0=BA=D0=B0,=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20APK=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=B9=20=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BA=D0=BE=D0=B9=20=D0=B8=20=D0=BF=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D0=B0=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + Assets/Editor/AndroidBuildFixes.cs | 61 +++++++++++ Assets/Editor/ApkBuilder.cs | 102 ++++++++++++++++++ Assets/Editor/BuildInfoWriter.cs | 51 +++++++++ Assets/Editor/BuildInfoWriter.cs.meta | 2 + Assets/Scripts/App/DebugPanel.cs | 15 +++ Assets/Scripts/App/GameVersionLabel.cs | 50 +++++++++ Assets/Scripts/App/GameVersionLabel.cs.meta | 2 + Assets/Scripts/City3D/City3DController.cs | 16 ++- .../Scripts/Modules/Settlement/BuildGrid.cs | 22 +++- ProjectSettings/EditorBuildSettings.asset | 3 + 11 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 Assets/Editor/AndroidBuildFixes.cs create mode 100644 Assets/Editor/ApkBuilder.cs create mode 100644 Assets/Editor/BuildInfoWriter.cs create mode 100644 Assets/Editor/BuildInfoWriter.cs.meta create mode 100644 Assets/Scripts/App/GameVersionLabel.cs create mode 100644 Assets/Scripts/App/GameVersionLabel.cs.meta diff --git a/.gitignore b/.gitignore index ee7c00e..6de267b 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,7 @@ crashlytics-build.properties /[Aa]ssets/[Ss]treamingAssets/aa.meta /[Aa]ssets/[Ss]treamingAssets/aa/* +# Сборки APK (Tools -> Bogatyri -> Собрать APK) и служебные папки сборки +/Builds/ +/build/ +/MVP1_BurstDebugInformation_DoNotShip/ diff --git a/Assets/Editor/AndroidBuildFixes.cs b/Assets/Editor/AndroidBuildFixes.cs new file mode 100644 index 0000000..5e4487e --- /dev/null +++ b/Assets/Editor/AndroidBuildFixes.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEditor.Android; +using UnityEngine; + +// Починка сборки Android на этом компьютере (05.11): +// 1. Имя пользователя Windows с русскими буквами (C:\Users\Вальдемар). Gradle хранит свои файлы в +// C:\Users\<имя>\.gradle, и одна из его программ (prefab) не понимает такой путь -- сборка падает. +// Перед сборкой переносим папку Gradle в C:\gradle (переменная GRADLE_USER_HOME для процесса Unity, +// сборщик её наследует). Ничего в Windows менять не нужно. +// 2. Два Kotlin в зависимостях (1.8 у Unity и 1.6 у одного из пакетов) -- "Duplicate class kotlin...". +// В сгенерированный проект Gradle дописываем правило: все kotlin-stdlib -- одной версии 1.8.22. +[InitializeOnLoad] +public class AndroidBuildFixes : IPostGenerateGradleAndroidProject +{ + const string GradleHome = @"C:\gradle"; + const string Marker = "// Bogatyri: kotlin-stdlib одной версии"; + + static AndroidBuildFixes() { EnsureAsciiGradleHome(); } + + public static void EnsureAsciiGradleHome() + { + string current = Environment.GetEnvironmentVariable("GRADLE_USER_HOME"); + if (!string.IsNullOrEmpty(current) && IsAscii(current)) return; + if (IsAscii(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))) return; // путь и так латиницей + try + { + Directory.CreateDirectory(GradleHome); + Environment.SetEnvironmentVariable("GRADLE_USER_HOME", GradleHome, EnvironmentVariableTarget.Process); + Debug.Log("[СБОРКА] Папка Gradle: " + GradleHome + " (в пути пользователя есть русские буквы)"); + } + catch (Exception e) { Debug.LogWarning("[СБОРКА] Не удалось задать GRADLE_USER_HOME: " + e.Message); } + } + + static bool IsAscii(string s) => !Regex.IsMatch(s ?? "", @"[^\u0000-\u007F]"); + + public int callbackOrder => 999; + + // path -- папка unityLibrary сгенерированного проекта + public void OnPostGenerateGradleAndroidProject(string path) + { + string root = Directory.GetParent(path).FullName; + foreach (var file in new[] { Path.Combine(root, "launcher", "build.gradle"), Path.Combine(path, "build.gradle") }) + { + if (!File.Exists(file)) continue; + string text = File.ReadAllText(file); + if (text.Contains(Marker)) continue; + text += "\n" + Marker + "\n" + + "configurations.all {\n" + + " resolutionStrategy.eachDependency { details ->\n" + + " if (details.requested.group == 'org.jetbrains.kotlin' && details.requested.name.startsWith('kotlin-stdlib')) {\n" + + " details.useVersion('1.8.22')\n" + + " }\n" + + " }\n" + + "}\n"; + File.WriteAllText(file, text); + } + } +} diff --git a/Assets/Editor/ApkBuilder.cs b/Assets/Editor/ApkBuilder.cs new file mode 100644 index 0000000..ffbdca3 --- /dev/null +++ b/Assets/Editor/ApkBuilder.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.Build.Reporting; + +// Сборка APK одной кнопкой (05.11): Tools -> Bogatyri -> Собрать APK. +// Файл: C:\MVP1\Builds\Bogatyri_<ветка>_v<версия>_<дата>.apk; сцены -- из списка сборки (первая -- стартовая). +// Итог сборки (успех, размер, первые ошибки) пишется в Builds\last_build.txt. +// Сборку можно запустить и файлом-запросом Builds\build_request.txt (так её запускает Claude): Unity заметит +// его в течение пары секунд, удалит и начнёт сборку. +[InitializeOnLoad] +public static class ApkBuilder +{ + const string Folder = "Builds"; + const string RequestFile = "Builds/build_request.txt"; + const string ResultFile = "Builds/last_build.txt"; + static double nextCheck; + + static ApkBuilder() + { + EditorApplication.update += Poll; + } + + static void Poll() + { + if (EditorApplication.timeSinceStartup < nextCheck) return; + nextCheck = EditorApplication.timeSinceStartup + 2; + if (!File.Exists(RequestFile) || BuildPipeline.isBuildingPlayer || EditorApplication.isCompiling || EditorApplication.isPlaying) return; + File.Delete(RequestFile); + EditorApplication.delayCall += Build; + } + + [MenuItem("Tools/Bogatyri/Собрать APK")] + public static void Build() + { + AndroidBuildFixes.EnsureAsciiGradleHome(); + Directory.CreateDirectory(Folder); + string branch = Git("rev-parse --abbrev-ref HEAD").Replace('/', '-'); + if (branch.Length == 0) branch = "local"; + string file = $"{Folder}/Bogatyri_{branch}_v{PlayerSettings.bundleVersion}_{DateTime.Now:yyyyMMdd_HHmm}.apk"; + var scenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray(); + + File.WriteAllText(ResultFile, $"Сборка идёт: {DateTime.Now:dd.MM.yyyy HH:mm}\n{file}\n"); + BuildReport report; + try + { + EditorUserBuildSettings.buildAppBundle = false; // обычный APK + report = BuildPipeline.BuildPlayer(new BuildPlayerOptions + { + scenes = scenes, + locationPathName = file, + target = BuildTarget.Android, + options = BuildOptions.None + }); + } + catch (Exception e) + { + File.WriteAllText(ResultFile, $"ОШИБКА {DateTime.Now:dd.MM.yyyy HH:mm}\n{e}\n"); + throw; + } + + var s = report.summary; + var text = new StringBuilder(); + text.AppendLine($"{(s.result == BuildResult.Succeeded ? "ГОТОВО" : "ОШИБКА")} {DateTime.Now:dd.MM.yyyy HH:mm} результат: {s.result}"); + text.AppendLine($"Файл: {Path.GetFullPath(file)}"); + if (s.result == BuildResult.Succeeded && File.Exists(file)) text.AppendLine($"Размер: {new FileInfo(file).Length / 1048576.0:0.0} МБ"); + text.AppendLine($"Версия: {PlayerSettings.bundleVersion}, код {PlayerSettings.Android.bundleVersionCode}, ветка {branch} {Git("rev-parse --short HEAD")}"); + text.AppendLine($"Сцены: {string.Join(", ", scenes)}"); + text.AppendLine($"Время: {s.totalTime}, ошибок: {s.totalErrors}, предупреждений: {s.totalWarnings}"); + int shown = 0; + foreach (var step in report.steps) + foreach (var m in step.messages) + if (m.type == UnityEngine.LogType.Error || m.type == UnityEngine.LogType.Exception) + { + if (shown++ >= 15) break; + string msg = m.content.Length > 1500 ? m.content.Substring(0, 1500) + " ..." : m.content; + text.AppendLine("--- " + msg); + } + File.WriteAllText(ResultFile, text.ToString()); + UnityEngine.Debug.Log("[СБОРКА] " + text.ToString().Split('\n')[0] + " -> " + file); + if (s.result == BuildResult.Succeeded) EditorUtility.RevealInFinder(file); + } + + static string Git(string args) + { + try + { + var p = Process.Start(new ProcessStartInfo("git", args) + { + RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, + WorkingDirectory = Directory.GetCurrentDirectory() + }); + string o = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(5000); + return o; + } + catch { return ""; } + } +} diff --git a/Assets/Editor/BuildInfoWriter.cs b/Assets/Editor/BuildInfoWriter.cs new file mode 100644 index 0000000..4dd8969 --- /dev/null +++ b/Assets/Editor/BuildInfoWriter.cs @@ -0,0 +1,51 @@ +using System; +using System.Diagnostics; +using System.IO; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; + +// Перед каждым билдом записывает в Assets/Resources/BuildInfo.txt: ветку, коммит и дату сборки (05.11). +// Игра показывает это в углу экрана рядом с версией (App/GameVersionLabel). После билда файл удаляется, +// чтобы не попадать в git. +public class BuildInfoWriter : IPreprocessBuildWithReport, IPostprocessBuildWithReport +{ + const string Path = "Assets/Resources/BuildInfo.txt"; + public int callbackOrder => 0; + + public void OnPreprocessBuild(BuildReport report) + { + string branch = Or(Git("rev-parse --abbrev-ref HEAD")), commit = Or(Git("rev-parse --short HEAD")); + bool dirty = Git("status --porcelain --untracked-files=no").Length > 0; + string info = $"{branch} {commit}{(dirty ? "+" : "")} • {DateTime.Now:dd.MM.yyyy HH:mm}"; + Directory.CreateDirectory("Assets/Resources"); + File.WriteAllText(Path, info); + AssetDatabase.ImportAsset(Path); + UnityEngine.Debug.Log("[ВЕРСИЯ] Сборка: v" + PlayerSettings.bundleVersion + " • " + info); + } + + public void OnPostprocessBuild(BuildReport report) + { + AssetDatabase.DeleteAsset(Path); + } + + static string Git(string args) + { + try + { + var p = Process.Start(new ProcessStartInfo("git", args) + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Directory.GetCurrentDirectory() + }); + string output = p.StandardOutput.ReadToEnd().Trim(); + p.WaitForExit(5000); + return output; + } + catch { return ""; } + } + + static string Or(string s) => string.IsNullOrEmpty(s) ? "?" : s; +} diff --git a/Assets/Editor/BuildInfoWriter.cs.meta b/Assets/Editor/BuildInfoWriter.cs.meta new file mode 100644 index 0000000..3f4d40e --- /dev/null +++ b/Assets/Editor/BuildInfoWriter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1218c67fef1faaf448c6623df897ac12 \ No newline at end of file diff --git a/Assets/Scripts/App/DebugPanel.cs b/Assets/Scripts/App/DebugPanel.cs index 655089f..d8a86f2 100644 --- a/Assets/Scripts/App/DebugPanel.cs +++ b/Assets/Scripts/App/DebugPanel.cs @@ -17,6 +17,7 @@ public class DebugPanel : MonoBehaviour GameObject panel; TextMeshProUGUI info; + UnityEngine.UI.Button gridButton; float nextRefresh; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] @@ -81,11 +82,25 @@ public class DebugPanel : MonoBehaviour var row4 = UIKit.Row(card, 70, 10); Btn(row4, "Рабочие", UIKit.Theme.Blue, () => BuildingWindow.Open(null)); + gridButton = UIKit.Button(row4, GridLabel, UIKit.Theme.Blue, ToggleGrid, -1, 70, 24); + UIKit.Size(gridButton, -1, 70, 1); Btn(row4, "Закрыть", UIKit.Theme.Gray, () => panel.SetActive(false)); panel.SetActive(false); } + // Отладочная сетка стройки (3D-поселение и 2D-город): видна всегда, ярче, каждая 5-я линия жёлтая (05.11, просьба Миши) + void ToggleGrid() + { + bool on = !(City3DController.DebugGrid || BuildGrid.DebugVisible); + City3DController.SetDebugGrid(on); + bool building = BuildModeController.Instance != null && BuildModeController.Instance.IsActive; + BuildGrid.SetDebugVisible(on, building); + if (gridButton != null) UIKit.SetButton(gridButton, GridLabel, true); + } + + static string GridLabel => City3DController.DebugGrid || BuildGrid.DebugVisible ? "Сетка: вкл" : "Сетка: выкл"; + static void Btn(Transform row, string label, Color color, System.Action onClick) { var b = UIKit.Button(row, label, color, onClick, -1, 70, 24); diff --git a/Assets/Scripts/App/GameVersionLabel.cs b/Assets/Scripts/App/GameVersionLabel.cs new file mode 100644 index 0000000..5573f06 --- /dev/null +++ b/Assets/Scripts/App/GameVersionLabel.cs @@ -0,0 +1,50 @@ +using UnityEngine; + +// Версия игры на экране (05.11, требование Миши: в каждом билде видно, что это за версия). +// В углу экрана: версия из Player Settings (Application.version) и сборка -- ветка, коммит, дата. +// Сборку пишет Editor/BuildInfoWriter перед билдом в Resources/BuildInfo.txt; в редакторе -- "редактор". +// Создаётся сам при запуске, живёт между сценами. +public class GameVersionLabel : MonoBehaviour +{ + static GameVersionLabel instance; + string text; + GUIStyle style, shadow; + + public static string BuildInfo + { + get + { + var asset = Resources.Load("BuildInfo"); + return asset != null ? asset.text.Trim() : "редактор"; + } + } + + public static string FullVersion => $"v{Application.version} • {BuildInfo}"; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void Create() + { + if (instance != null) return; + var go = new GameObject("GameVersionLabel"); + DontDestroyOnLoad(go); + instance = go.AddComponent(); + instance.text = FullVersion; + Debug.Log("[ВЕРСИЯ] " + instance.text); + } + + void OnGUI() + { + if (style == null) + { + int size = Mathf.Max(12, Screen.height / 60); + style = new GUIStyle(GUI.skin.label) { fontSize = size, alignment = TextAnchor.LowerLeft }; + style.normal.textColor = new Color(1f, 1f, 1f, 0.75f); + shadow = new GUIStyle(style); + shadow.normal.textColor = new Color(0f, 0f, 0f, 0.6f); + } + GUI.depth = -1000; // поверх всего + var r = new Rect(8, Screen.height - style.fontSize * 2 - 4, Screen.width * 0.6f, style.fontSize * 2); + GUI.Label(new Rect(r.x + 1, r.y + 1, r.width, r.height), text, shadow); + GUI.Label(r, text, style); + } +} diff --git a/Assets/Scripts/App/GameVersionLabel.cs.meta b/Assets/Scripts/App/GameVersionLabel.cs.meta new file mode 100644 index 0000000..a98ead1 --- /dev/null +++ b/Assets/Scripts/App/GameVersionLabel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4dae86c365fe55948b878547633d712a \ No newline at end of file diff --git a/Assets/Scripts/City3D/City3DController.cs b/Assets/Scripts/City3D/City3DController.cs index c9b88db..82b72a5 100644 --- a/Assets/Scripts/City3D/City3DController.cs +++ b/Assets/Scripts/City3D/City3DController.cs @@ -99,6 +99,17 @@ public class City3DController : MonoBehaviour, City3DSave.ILive if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll(); } + // Отладочная сетка (05.11, просьба Миши): видна всегда, ярче, каждая 5-я линия жёлтая -- примерять референсы. + // Включается кнопкой "Сетка" в панели DEV. + public static bool DebugGrid { get; private set; } + + public static void SetDebugGrid(bool on) + { + DebugGrid = on; + var city = FindAnyObjectByType(); + if (city != null && city.map != null) city.RefreshGround(); + } + bool stateApplied; // сохранённый город уже поставлен (в первом кадре, после загрузки сохранения в Start) void OnEnable() @@ -324,7 +335,7 @@ public class City3DController : MonoBehaviour, City3DSave.ILive void RefreshGround() { int n = City3DLayout.Size, px = PixelsPerCell; - bool grid = mode != Mode.View; + bool grid = mode != Mode.View || DebugGrid; var pixels = new Color32[n * px * n * px]; for (int y = 0; y < n; y++) for (int x = 0; x < n; x++) @@ -349,7 +360,8 @@ public class City3DController : MonoBehaviour, City3DSave.ILive for (int ix = 0; ix < px; ix++) { Color p = c; - if (grid && (ix == 0 || iy == 0)) p *= 0.85f; // линии клеток + if (grid && (ix == 0 || iy == 0)) p *= DebugGrid ? 0.55f : 0.85f; // линии клеток + if (DebugGrid && ((ix == 0 && x % 5 == 0) || (iy == 0 && y % 5 == 0))) p = new Color(1f, 0.85f, 0.2f); // каждая 5-я // граница открытой зоны -- светлая линия if (grid && open && (ix == 0 && !map.IsOpen(x - 1, y, townHallLevel) || ix == px - 1 && !map.IsOpen(x + 1, y, townHallLevel) || iy == 0 && !map.IsOpen(x, y - 1, townHallLevel) || iy == px - 1 && !map.IsOpen(x, y + 1, townHallLevel))) diff --git a/Assets/Scripts/Modules/Settlement/BuildGrid.cs b/Assets/Scripts/Modules/Settlement/BuildGrid.cs index 704826e..985fc46 100644 --- a/Assets/Scripts/Modules/Settlement/BuildGrid.cs +++ b/Assets/Scripts/Modules/Settlement/BuildGrid.cs @@ -35,6 +35,20 @@ public class BuildGrid : MonoBehaviour [Tooltip("Посчитать область самому: старые участки + Ратуша + запас")] public bool autoArea = true; + // Отладка (05.11, просьба Миши): сетка видна всегда и ярче, каждая 5-я линия жирнее -- чтобы примерять + // к ней размеры зданий и референсы. Включается кнопкой "Сетка" в панели DEV. + public static bool DebugVisible { get; private set; } + + public static void SetDebugVisible(bool on, bool buildModeActive) + { + DebugVisible = on; + if (BuildManager.Instance == null) return; // 2D-город не открыт -- сетка появится, когда откроется + var grid = Instance; + if (grid == null) return; + grid.lastOverlayKey = null; + grid.ShowOverlay(on || buildModeActive); + } + // Здание, которое сейчас переносят: его клетки считаются свободными (режим стройки, перенос) public static GameObject IgnoredBuilding; [Tooltip("Запас вокруг старых участков, клеток")] @@ -258,6 +272,7 @@ public class BuildGrid : MonoBehaviour public void ShowOverlay(bool show) { EnsureInit(); + show |= DebugVisible; // отладочная сетка не прячется при выходе из режима стройки if (show && overlay == null) CreateOverlay(); if (overlay == null) return; overlay.enabled = show; @@ -283,7 +298,8 @@ public class BuildGrid : MonoBehaviour void RedrawOverlay() { // перерисовываем только если занятость поменялась - var key = new System.Text.StringBuilder(Width * Height); + var key = new System.Text.StringBuilder(Width * Height + 1); + key.Append(DebugVisible ? 'D' : 'n'); for (int y = 0; y < Height; y++) for (int x = 0; x < Width; x++) key.Append(busy[x, y] ? '1' : '0'); string k = key.ToString(); if (k == lastOverlayKey) return; @@ -291,7 +307,8 @@ public class BuildGrid : MonoBehaviour int w = overlayTex.width, h = overlayTex.height; var px = new Color32[w * h]; - Color32 line = lineColor, free = freeColor, bad = busyColor; + Color32 line = DebugVisible ? new Color(1f, 1f, 1f, 0.7f) : lineColor, free = freeColor, bad = busyColor; + Color32 major = new Color32(255, 230, 80, 230); // каждая 5-я клетка -- жёлтая линия (только в отладке) for (int y = 0; y < h; y++) { int cy = y / PixelsPerCell, iy = y % PixelsPerCell; @@ -300,6 +317,7 @@ public class BuildGrid : MonoBehaviour int cx = x / PixelsPerCell, ix = x % PixelsPerCell; bool edge = ix == 0 || iy == 0 || x == w - 1 || y == h - 1; px[y * w + x] = busy[cx, cy] ? bad : (edge ? line : free); + if (DebugVisible && ((ix == 0 && cx % 5 == 0) || (iy == 0 && cy % 5 == 0))) px[y * w + x] = major; if (busy[cx, cy] && edge) px[y * w + x] = new Color32(bad.r, bad.g, bad.b, (byte)Mathf.Min(255, bad.a + 50)); } } diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset index 975628f..73db522 100644 --- a/ProjectSettings/EditorBuildSettings.asset +++ b/ProjectSettings/EditorBuildSettings.asset @@ -8,6 +8,9 @@ EditorBuildSettings: - enabled: 1 path: Assets/Scenes/MVP1.unity guid: beae4025d64976c4f93af3b9233183da + - enabled: 1 + path: Assets/Scenes/City3D.unity + guid: c1c34719f0605c247a3575d077f3d656 - enabled: 1 path: Assets/Scenes/WorldMap.unity guid: baa48fafda29f4247b2910dc9622c97a