This commit is contained in:
2026-10-06 16:26:20 +05:30
parent d4c7f5dd46
commit 7fa0621ff8
230 changed files with 16821 additions and 55 deletions
+2 -2
View File
@@ -297,7 +297,7 @@ public class LoginManager : MonoBehaviour
public static void LogoutAndReturnToLogin()
{
Logout();
SceneManager.LoadScene(0);
AddressableSceneLoader.Load(AddressableSceneLoader.IntroScene);
}
static void ProceedToMainMenu()
@@ -305,7 +305,7 @@ public class LoginManager : MonoBehaviour
if (LevelLoadManager.instance != null)
LevelLoadManager.LoadLevel("MainMenu");
else
SceneManager.LoadScene("MainMenu");
AddressableSceneLoader.Load(AddressableSceneLoader.MainMenuScene);
}
void SetBusy(bool busy)
+1 -1
View File
@@ -83,7 +83,7 @@ public class MainMenuManager : MonoBehaviour
void Awake()
{
if(LoginManager.instance==null){
SceneManager.LoadScene(0);
AddressableSceneLoader.Load(AddressableSceneLoader.IntroScene);
}
instance = this;
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26c7172a22b6197419c452a40e4b22f9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,62 @@
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
public static class AddressableSceneLoader
{
public const string IntroScene = "Intro";
public const string MainMenuScene = "MainMenu";
public const string GameScene = "Game";
public static void Load(string sceneName)
{
AddressableSceneLoadRunner.Instance.Begin(sceneName);
}
public static IEnumerator LoadRoutine(string sceneName)
{
AsyncOperationHandle<SceneInstance> handle = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
yield return handle;
if (handle.Status != AsyncOperationStatus.Succeeded)
Debug.LogError("Failed to load addressable scene '" + sceneName + "': " + handle.OperationException);
}
}
public class AddressableSceneLoadRunner : MonoBehaviour
{
static AddressableSceneLoadRunner instance;
public static AddressableSceneLoadRunner Instance
{
get
{
if (instance != null)
return instance;
var go = new GameObject(nameof(AddressableSceneLoadRunner));
DontDestroyOnLoad(go);
instance = go.AddComponent<AddressableSceneLoadRunner>();
return instance;
}
}
void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
}
public void Begin(string sceneName)
{
StartCoroutine(AddressableSceneLoader.LoadRoutine(sceneName));
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14a07839b30f1f34ab2d48cb46340278
+238
View File
@@ -0,0 +1,238 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.Networking;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.UI;
using TMPro;
public class BootstrapUpdater : MonoBehaviour
{
public const string ContentReadyPrefsKey = "Ota.ContentReady";
[Header("Host")]
[Tooltip("Apache folder that serves version.json and the Addressables catalog. Example: https://kkapi.playpoolstudios.com/ota")]
[SerializeField] string otaBaseUrl = "https://kkapi.playpoolstudios.com/ota";
[Tooltip("Opened when version.json has no storeUrl. Same role as the login screen Telegram link.")]
[SerializeField] string fallbackStoreUrl = "https://t.me/+dfUhy4gmGXk0ZDRl";
[Header("Content")]
[SerializeField] string introSceneAddress = AddressableSceneLoader.IntroScene;
[SerializeField] string gameContentLabel = "game-content";
[Header("UI")]
[SerializeField] TMP_Text statusText;
[SerializeField] Slider progressSlider;
[SerializeField] TMP_Text progressText;
[SerializeField] GameObject retryButton;
[SerializeField] GameObject updateButton;
string pendingStoreUrl = "";
bool updateRunning;
void Start()
{
SetRetryVisible(false);
SetUpdateVisible(false);
SetProgress(0f, "");
if (!updateRunning)
StartCoroutine(RunUpdate());
}
public void Retry()
{
if (updateRunning)
return;
SetRetryVisible(false);
SetUpdateVisible(false);
StartCoroutine(RunUpdate());
}
public void OpenStore()
{
string url = string.IsNullOrWhiteSpace(pendingStoreUrl) ? fallbackStoreUrl : pendingStoreUrl;
if (!string.IsNullOrWhiteSpace(url))
Application.OpenURL(url.Trim());
}
IEnumerator RunUpdate()
{
updateRunning = true;
SetStatus("Checking for updates");
OtaVersionInfo version = null;
bool versionFetched = false;
yield return FetchVersion(info =>
{
version = info;
versionFetched = info != null;
});
if (!versionFetched)
{
#if UNITY_EDITOR
Debug.LogWarning("OTA version.json was not reachable. Editor play continues into " + introSceneAddress + ".");
#else
if (PlayerPrefs.GetInt(ContentReadyPrefsKey, 0) != 1)
{
Fail("Check your connection and try again.");
yield break;
}
SetStatus("Offline. Loading saved game");
#endif
}
else if (OtaVersionInfo.IsClientBelowRequiredVersion(version.minPlayerVersion))
{
pendingStoreUrl = version.storeUrl;
string message = string.IsNullOrWhiteSpace(version.message)
? "A new version is required."
: version.message.Trim();
SetStatus(message);
SetUpdateVisible(true);
updateRunning = false;
yield break;
}
AsyncOperationHandle<IResourceLocator> init = Addressables.InitializeAsync(false);
yield return init;
bool initOk = init.IsValid() && init.Status == AsyncOperationStatus.Succeeded;
if (init.IsValid())
Addressables.Release(init);
if (!initOk)
{
Fail("Could not start content update.");
yield break;
}
AsyncOperationHandle<System.Collections.Generic.List<string>> check = Addressables.CheckForCatalogUpdates(false);
yield return check;
if (check.Status == AsyncOperationStatus.Succeeded && check.Result != null && check.Result.Count > 0)
{
SetStatus("Updating catalog");
AsyncOperationHandle<System.Collections.Generic.List<IResourceLocator>> catalogs = Addressables.UpdateCatalogs(check.Result, false);
yield return catalogs;
if (catalogs.Status != AsyncOperationStatus.Succeeded)
{
Addressables.Release(catalogs);
Addressables.Release(check);
Fail("Could not update the content catalog.");
yield break;
}
Addressables.Release(catalogs);
}
Addressables.Release(check);
long downloadBytes = 0;
AsyncOperationHandle<long> sizeHandle = Addressables.GetDownloadSizeAsync(gameContentLabel);
yield return sizeHandle;
if (sizeHandle.Status == AsyncOperationStatus.Succeeded)
downloadBytes = sizeHandle.Result;
Addressables.Release(sizeHandle);
if (downloadBytes > 0)
{
AsyncOperationHandle download = Addressables.DownloadDependenciesAsync(gameContentLabel, false);
while (!download.IsDone)
{
DownloadStatus status = download.GetDownloadStatus();
float percent = status.TotalBytes > 0
? (float)status.DownloadedBytes / status.TotalBytes
: download.PercentComplete;
SetProgress(percent, FormatMegabytes(status.DownloadedBytes) + " / " + FormatMegabytes(status.TotalBytes));
SetStatus("Downloading update " + Mathf.RoundToInt(percent * 100f) + "%");
yield return null;
}
if (download.Status != AsyncOperationStatus.Succeeded)
{
Addressables.Release(download);
Fail("Download failed. Check your connection and try again.");
yield break;
}
Addressables.Release(download);
}
PlayerPrefs.SetInt(ContentReadyPrefsKey, 1);
PlayerPrefs.Save();
SetStatus("Loading");
SetProgress(1f, "");
yield return AddressableSceneLoader.LoadRoutine(introSceneAddress);
updateRunning = false;
}
IEnumerator FetchVersion(Action<OtaVersionInfo> onResult)
{
string baseUrl = (otaBaseUrl ?? "").Trim().TrimEnd('/');
if (string.IsNullOrEmpty(baseUrl))
{
onResult?.Invoke(null);
yield break;
}
string url = baseUrl + "/version.json";
using (var request = UnityWebRequest.Get(url))
{
request.timeout = 20;
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
onResult?.Invoke(null);
yield break;
}
try
{
onResult?.Invoke(JsonUtility.FromJson<OtaVersionInfo>(request.downloadHandler.text));
}
catch (Exception e)
{
Debug.LogWarning("OTA version.json parse failed: " + e.Message);
onResult?.Invoke(null);
}
}
}
void Fail(string message)
{
SetStatus(message);
SetRetryVisible(true);
updateRunning = false;
}
void SetStatus(string message)
{
if (statusText != null)
statusText.text = message;
}
void SetProgress(float normalized, string label)
{
if (progressSlider != null)
{
progressSlider.gameObject.SetActive(true);
progressSlider.value = Mathf.Clamp01(normalized);
}
if (progressText != null)
progressText.text = label ?? "";
}
void SetRetryVisible(bool visible)
{
if (retryButton != null)
retryButton.SetActive(visible);
}
void SetUpdateVisible(bool visible)
{
if (updateButton != null)
updateButton.SetActive(visible);
}
static string FormatMegabytes(long bytes)
{
float mb = bytes / (1024f * 1024f);
return mb.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " MB";
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 095b27b1798839741b0d87da01566a79
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4a8936cb3c5731542a2e644e3ebd4b24
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,155 @@
#if UNITY_EDITOR
using System.IO;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEngine;
public static class OtaAddressablesSetup
{
const string RemoteLoadUrl = "https://kkapi.playpoolstudios.com/ota/[BuildTarget]";
const string GameContentLabel = "game-content";
const string BootstrapScenePath = "Assets/Scenes/Bootstrap.unity";
static readonly string[] RemoteScenePaths =
{
"Assets/Scenes/Intro.unity",
"Assets/Scenes/MainMenu.unity",
"Assets/Scenes/Game.unity"
};
[MenuItem("Tools/OTA/Configure Addressables")]
public static void Configure()
{
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.GetSettings(true);
settings.BuildRemoteCatalog = true;
settings.DisableCatalogUpdateOnStartup = true;
settings.OverridePlayerVersion = "";
string profileId = settings.activeProfileId;
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteBuildPath, "ServerData/[BuildTarget]");
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteLoadPath, RemoteLoadUrl);
settings.RemoteCatalogBuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
settings.RemoteCatalogLoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
for (int i = 0; i < settings.DataBuilders.Count; i++)
{
if (settings.GetDataBuilder(i) != null && settings.GetDataBuilder(i).Name.Contains("Asset Database"))
{
settings.ActivePlayModeDataBuilderIndex = i;
break;
}
}
AddressableAssetGroup local = GetOrCreateGroup(settings, "Local_Bootstrap");
AddressableAssetGroup remote = GetOrCreateGroup(settings, "Remote_Game");
ConfigureGroup(settings, local, true);
ConfigureGroup(settings, remote, false);
MarkScene(settings, local, BootstrapScenePath, "Bootstrap", false);
for (int i = 0; i < RemoteScenePaths.Length; i++)
{
string path = RemoteScenePaths[i];
string address = Path.GetFileNameWithoutExtension(path);
MarkScene(settings, remote, path, address, true);
}
settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
AssetDatabase.SaveAssets();
var scenes = new EditorBuildSettingsScene[1];
scenes[0] = new EditorBuildSettingsScene(BootstrapScenePath, true);
EditorBuildSettings.scenes = scenes;
Debug.Log("OTA Addressables configured. Remote load path: " + RemoteLoadUrl);
}
[MenuItem("Tools/OTA/Build Content")]
public static void BuildContent()
{
Configure();
AddressableAssetSettings.BuildPlayerContent(out AddressablesPlayerBuildResult result);
if (!string.IsNullOrEmpty(result.Error))
{
Debug.LogError("OTA content build failed: " + result.Error);
return;
}
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
Debug.Log("OTA content build finished. Keep this file for later patches: " + statePath);
}
[MenuItem("Tools/OTA/Build Content Update")]
public static void BuildContentUpdate()
{
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogError("Addressables settings are missing. Run Tools/OTA/Configure Addressables first.");
return;
}
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
if (!File.Exists(statePath))
{
Debug.LogError("Missing content state at " + statePath + ". Run Tools/OTA/Build Content from the player build you shipped.");
return;
}
ContentUpdateScript.BuildContentUpdate(settings, statePath);
Debug.Log("OTA content update built. Upload ServerData and keep " + statePath);
}
static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string name)
{
AddressableAssetGroup group = settings.FindGroup(name);
if (group != null)
return group;
return settings.CreateGroup(name, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema));
}
static void ConfigureGroup(AddressableAssetSettings settings, AddressableAssetGroup group, bool local)
{
BundledAssetGroupSchema bundle = group.GetSchema<BundledAssetGroupSchema>();
if (bundle == null)
bundle = group.AddSchema<BundledAssetGroupSchema>();
if (local)
{
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kLocalBuildPath);
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kLocalLoadPath);
}
else
{
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
}
bundle.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;
bundle.UseAssetBundleCrc = true;
bundle.UseAssetBundleCache = true;
ContentUpdateGroupSchema update = group.GetSchema<ContentUpdateGroupSchema>();
if (update == null)
update = group.AddSchema<ContentUpdateGroupSchema>();
update.StaticContent = local;
}
static void MarkScene(AddressableAssetSettings settings, AddressableAssetGroup group, string assetPath, string address, bool gameContent)
{
string guid = AssetDatabase.AssetPathToGUID(assetPath);
if (string.IsNullOrEmpty(guid))
{
Debug.LogWarning("OTA scene missing, skipped: " + assetPath);
return;
}
AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, group, false, false);
entry.address = address;
if (gameContent)
entry.SetLabel(GameContentLabel, true, true, true);
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 563b5612b6e2b074e91bc5b86be879e2
+31
View File
@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using UnityEngine;
[Serializable]
public class OtaVersionInfo
{
public string minPlayerVersion = "";
public string contentVersion = "";
public string message = "";
public string storeUrl = "";
public static bool IsClientBelowRequiredVersion(string requiredVersion)
{
if (string.IsNullOrWhiteSpace(requiredVersion))
return false;
if (!TryParseVersionFloat(requiredVersion, out float required))
return false;
if (!TryParseVersionFloat(Application.version, out float client))
return false;
return client < required;
}
public static bool TryParseVersionFloat(string version, out float value)
{
value = 0f;
if (string.IsNullOrWhiteSpace(version))
return false;
return float.TryParse(version.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8e1e50b8157291245a0f59a9d20a194f
+1 -2
View File
@@ -1,10 +1,9 @@
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneManagerUIHelper : MonoBehaviour
{
public void LoadScene(string sceneName){
SceneManager.LoadScene(sceneName);
AddressableSceneLoader.Load(sceneName);
}
public void Quit(){
@@ -66,8 +66,7 @@ public class LevelLoadManager : MonoBehaviour
if (mainMenuDestination)
BlocksMainMenuSettingsBootstrap = true;
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(levelName);
yield return asyncLoad;
yield return AddressableSceneLoader.LoadRoutine(levelName);
if (mainMenuDestination)
{