Compare commits

...
13 Commits
Author SHA1 Message Date
warlock 5ac8f92006 qa report v2 2026-09-06 21:36:26 +05:30
warlock 78592e8d8e rc 2026-08-20 19:45:38 +05:30
warlock 74d9fae6e6 forfeiture, abandoning, entry_fee_collection at start 2026-08-14 11:13:01 +05:30
warlock 9970488938 unity error 2026-08-12 13:45:19 +05:30
warlock c2f4c2f808 migration 2026-07-26 12:31:59 +05:30
warlock 4b2129b944 practice mode integrated to the onboarding tutorial 2026-07-04 00:41:46 +05:30
warlock 2de5a166f6 practice mode 2026-07-02 11:41:57 +05:30
warlock 2651068b50 ping reporting 2026-06-02 14:47:32 +05:30
warlock d62bf9e06b test week rc 2026-05-29 10:02:24 +05:30
warlock 0a1d3c7262 sync 2026-05-27 23:24:40 +05:30
warlock f3e01462c1 not enough rc for rematch 2026-05-19 01:17:02 +05:30
warlock 87301a228f disonncet grace improved 2026-05-17 20:11:42 +05:30
warlock aa924c71dc rematch use the same leave function, added a grace period for connection disconnect 2026-05-17 14:50:28 +05:30
797 changed files with 84698 additions and 1530 deletions
+3 -3
View File
@@ -60,11 +60,11 @@
"*.asset": "yaml", "*.asset": "yaml",
"*.meta": "yaml", "*.meta": "yaml",
"*.prefab": "yaml", "*.prefab": "yaml",
"*.unity": "yaml", "*.unity": "yaml"
}, },
"explorer.fileNesting.enabled": true, "explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.sln": "*.csproj", "*.sln": "*.csproj"
}, },
"dotnet.defaultSolution": "WalkInvest_Soccer.sln" "dotnet.defaultSolution": "soccar2d.sln"
} }
+12 -2
View File
@@ -14,7 +14,8 @@ public class CupidConnector : MonoBehaviour
//Server code //Server code
string[] args = Environment.GetCommandLineArgs(); string[] args = Environment.GetCommandLineArgs();
int matchId = 0; int matchId = 0;
string dedicatedSecret = "38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328"; string dedicatedSecret = null;
bool dedicatedSecretFromCli = false;
string internalApiBase = null; string internalApiBase = null;
for (int i = 0; i < args.Length; i++) for (int i = 0; i < args.Length; i++)
{ {
@@ -28,12 +29,21 @@ public class CupidConnector : MonoBehaviour
|| string.Equals(args[i], "-matchid", StringComparison.OrdinalIgnoreCase)) || string.Equals(args[i], "-matchid", StringComparison.OrdinalIgnoreCase))
int.TryParse(args[i + 1], out matchId); int.TryParse(args[i + 1], out matchId);
else if (string.Equals(args[i], "-dedicatedSecret", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(args[i], "-dedicatedSecret", StringComparison.OrdinalIgnoreCase))
{
dedicatedSecret = args[i + 1]; dedicatedSecret = args[i + 1];
dedicatedSecretFromCli = true;
}
else if (string.Equals(args[i], "-internalApiBase", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(args[i], "-internalApiBase", StringComparison.OrdinalIgnoreCase))
internalApiBase = args[i + 1]; internalApiBase = args[i + 1];
} }
} }
GameManager.ConfigureDedicatedMatchReporting(matchId); if (!dedicatedSecretFromCli)
{
string envSecret = Environment.GetEnvironmentVariable("DEDICATED_SERVER_SECRET");
if (!string.IsNullOrEmpty(envSecret))
dedicatedSecret = envSecret;
}
GameManager.ConfigureDedicatedMatchReporting(matchId, dedicatedSecret, internalApiBase);
#if UNITY_EDITOR #if UNITY_EDITOR
Logger.Log("Editor mode, skipping arg check"); Logger.Log("Editor mode, skipping arg check");
Cupid.RoomPort = 7777; Cupid.RoomPort = 7777;
+26
View File
@@ -31,6 +31,12 @@ public static class Cupid
public static int RoomPort = -1; public static int RoomPort = -1;
/// <summary>Clears auth/matchmaker session fields. Server address is left so the next login can <see cref="Init"/> again.</summary>
public static void ClearSession()
{
bearerToken = "";
RoomPort = -1;
}
public static async Task Init(string _serverAddress, int _port, string _bearerToken, string _settingsPassword) public static async Task Init(string _serverAddress, int _port, string _bearerToken, string _settingsPassword)
{ {
@@ -264,12 +270,32 @@ public class CupidSettings
public int waiting_time; public int waiting_time;
public int port_range_min; public int port_range_min;
public int port_range_max; public int port_range_max;
public CupidGameEntry[] games;
/// <summary>RC stake per player for the default match (<c>/settings</c>), in coin units (same as matchmaker <c>entry_fee</c>).</summary> /// <summary>RC stake per player for the default match (<c>/settings</c>), in coin units (same as matchmaker <c>entry_fee</c>).</summary>
public int entry_fee; public int entry_fee;
/// <summary>Optional side-bet or table minimum from matchmaker settings; same units as <see cref="entry_fee"/>.</summary> /// <summary>Optional side-bet or table minimum from matchmaker settings; same units as <see cref="entry_fee"/>.</summary>
public int bet_fee; public int bet_fee;
/// <summary>Winner net RC prize from settings, coin units (same as room <c>rc_prize</c>).</summary> /// <summary>Winner net RC prize from settings, coin units (same as room <c>rc_prize</c>).</summary>
public int rc_prize; public int rc_prize;
public CupidTableSettings table_settings;
}
[System.Serializable]
public class CupidGameEntry
{
public string name;
public string exe;
}
[System.Serializable]
public class CupidTableSettings
{
public string entry_fee;
public string bet_fee;
/// <summary>Minimum client version (e.g. <c>0.99</c>, <c>1.21</c>). Compared to <see cref="Application.version"/>.</summary>
public string version;
public string error_msg;
public string warning_msg;
} }
[System.Serializable] [System.Serializable]
+6
View File
@@ -51,6 +51,12 @@ public class CupidLobby : MonoBehaviour
} }
public static CupidLobby instance; public static CupidLobby instance;
public static void ClearUsername()
{
m_username = "";
}
void Awake(){ void Awake(){
instance=this; instance=this;
} }
+59 -2
View File
@@ -6,6 +6,8 @@ public class Logger
{ {
public static bool Enabled = true; public static bool Enabled = true;
private static Logger m_instance = null; private static Logger m_instance = null;
private static readonly object FileLock = new object();
private static string ApplicationDirectory private static string ApplicationDirectory
{ {
get get
@@ -18,6 +20,12 @@ public class Logger
} }
string path = Application.dataPath; string path = Application.dataPath;
#if UNITY_EDITOR
// Never write under Assets/: the Editor importer locks those files, and
// File.AppendAllText then throws Sharing violation. Mirror treats that as a
// fatal RPC error and disconnects the client.
path = Path.GetFullPath(Path.Combine(path, ".."));
#else
if (Application.platform == RuntimePlatform.OSXPlayer) if (Application.platform == RuntimePlatform.OSXPlayer)
{ {
path = Path.Combine(path, "..", ".."); path = Path.Combine(path, "..", "..");
@@ -26,6 +34,7 @@ public class Logger
{ {
path = Path.Combine(path, ".."); path = Path.Combine(path, "..");
} }
#endif
return path; return path;
} }
} }
@@ -50,14 +59,14 @@ public class Logger
if(LogFilePath == null){ if(LogFilePath == null){
LogFilePath = Path.Combine(ApplicationDirectory, "Log.txt"); LogFilePath = Path.Combine(ApplicationDirectory, "Log.txt");
} }
File.WriteAllText(LogFilePath, "Logger initiated at " + DateTime.Now + "\n\n"); TryWriteFile(LogFilePath, "Logger initiated at " + DateTime.Now + "\n\n", append: false);
} }
public void log(string message){ public void log(string message){
if(!Enabled){return;} if(!Enabled){return;}
File.AppendAllText(LogFilePath,$"[{DateTime.Now}] {message}\n");
Debug.Log(message); Debug.Log(message);
TryWriteFile(LogFilePath, $"[{DateTime.Now}] {message}\n", append: true);
} }
public static void Log(string message){ public static void Log(string message){
@@ -75,7 +84,55 @@ public class Logger
} }
} }
/// <summary>Writable Logs folder next to match log files (creates if missing).</summary>
public static string GetLogsDirectory()
{
string dir = Path.Combine(ApplicationDirectory, "Logs");
EnsureDirectoryExists(dir);
return dir;
}
public static void SetFilePath(string path){ public static void SetFilePath(string path){
instance.LogFilePath= path; instance.LogFilePath= path;
} }
/// <summary>
/// File I/O must never throw: callers include Mirror RPCs, and an exception there
/// disconnects the client. Sharing violations are expected if another process has
/// the file open; FileShare.ReadWrite plus a swallow keeps gameplay alive.
/// </summary>
static void TryWriteFile(string path, string contents, bool append)
{
if (string.IsNullOrEmpty(path) || contents == null)
return;
try
{
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
EnsureDirectoryExists(dir);
}
catch (Exception e)
{
Debug.LogWarning("Logger: could not create log directory: " + e.Message);
return;
}
lock (FileLock)
{
try
{
FileMode mode = append ? FileMode.Append : FileMode.Create;
using (var stream = new FileStream(path, mode, FileAccess.Write, FileShare.ReadWrite))
using (var writer = new StreamWriter(stream))
{
writer.Write(contents);
}
}
catch (Exception e)
{
Debug.LogWarning("Logger: file write failed (" + path + "): " + e.Message);
}
}
}
} }
@@ -777,6 +777,66 @@ MonoBehaviour:
m_XAdvance: 0 m_XAdvance: 0
m_YAdvance: 0 m_YAdvance: 0
m_FeatureLookupFlags: 1504773 m_FeatureLookupFlags: 1504773
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 5
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.08
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 14
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 31
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.08
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 40
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord: - m_FirstAdjustmentRecord:
m_GlyphIndex: 12 m_GlyphIndex: 12
m_GlyphValueRecord: m_GlyphValueRecord:
@@ -1647,6 +1707,456 @@ MonoBehaviour:
m_XAdvance: 0 m_XAdvance: 0
m_YAdvance: 0 m_YAdvance: 0
m_FeatureLookupFlags: 10188005 m_FeatureLookupFlags: 10188005
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -2.97
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 5
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 6
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 8
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 9
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 12
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 13
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 15
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 16
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 17
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 18
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 20
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 22
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.26
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 23
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.99
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 30
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -2.97
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 31
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 32
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 34
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 35
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 36
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 38
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 39
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 42
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 43
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 44
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 46
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.45000002
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 48
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.26
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 49
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord:
m_GlyphIndex: 25
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.99
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 56
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 1016088
- m_FirstAdjustmentRecord: - m_FirstAdjustmentRecord:
m_GlyphIndex: 26 m_GlyphIndex: 26
m_GlyphValueRecord: m_GlyphValueRecord:
@@ -3687,6 +4197,66 @@ MonoBehaviour:
m_XAdvance: 0 m_XAdvance: 0
m_YAdvance: 0 m_YAdvance: 0
m_FeatureLookupFlags: 10188005 m_FeatureLookupFlags: 10188005
- m_FirstAdjustmentRecord:
m_GlyphIndex: 36
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 5
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 36
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.08
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 14
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 36
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 31
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 36
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.08
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 40
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord: - m_FirstAdjustmentRecord:
m_GlyphIndex: 38 m_GlyphIndex: 38
m_GlyphValueRecord: m_GlyphValueRecord:
@@ -6447,6 +7017,216 @@ MonoBehaviour:
m_XAdvance: 0 m_XAdvance: 0
m_YAdvance: 0 m_YAdvance: 0
m_FeatureLookupFlags: 10188005 m_FeatureLookupFlags: 10188005
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -6.6600003
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 5
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -4.23
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 7
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -4.23
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 11
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 14
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -3.42
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 19
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -3.42
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 21
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -3.6000001
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 23
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -6.6600003
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 31
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -4.23
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 33
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -4.23
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 37
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 40
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -3.42
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 45
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -3.42
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 47
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord:
m_GlyphIndex: 55
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -3.6000001
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 49
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 11484403
- m_FirstAdjustmentRecord: - m_FirstAdjustmentRecord:
m_GlyphIndex: 56 m_GlyphIndex: 56
m_GlyphValueRecord: m_GlyphValueRecord:
+60
View File
@@ -777,6 +777,66 @@ MonoBehaviour:
m_XAdvance: 0 m_XAdvance: 0
m_YAdvance: 0 m_YAdvance: 0
m_FeatureLookupFlags: 1118827 m_FeatureLookupFlags: 1118827
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 5
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.08
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 14
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -5.8500004
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 31
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord:
m_GlyphIndex: 10
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.08
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 40
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: 2038298
- m_FirstAdjustmentRecord: - m_FirstAdjustmentRecord:
m_GlyphIndex: 12 m_GlyphIndex: 12
m_GlyphValueRecord: m_GlyphValueRecord:
+33 -24
View File
@@ -1,25 +1,34 @@
Logger initiated at 5/16/2026 11:47:02 PM Logger initiated at 8/27/2026 1:43:47 PM
[5/16/2026 11:47:02 PM] Cupid settings updated [8/27/2026 1:43:47 PM] Cupid settings updated
[5/16/2026 11:47:02 PM] Cupid settings updated [8/27/2026 1:43:48 PM] Cupid settings updated
[5/16/2026 11:47:10 PM] Starting matchmake as warlock [8/27/2026 1:43:50 PM] Starting matchmake as warlock
[5/16/2026 11:47:13 PM] Got into a room [8/27/2026 1:43:51 PM] Starting matchmake as warlock2
[5/16/2026 11:47:13 PM] {"ok":true,"entry_fee":21,"rc_prize":34,"for_red":{"Players":[{"Name":"warlock2","LastSeen":1778955432808,"UserId":3,"l10_wins":1,"l10_losses":3},{"Name":"warlock","LastSeen":1778955433214,"UserId":2,"l10_wins":3,"l10_losses":1}],"GameName":"soccar","Port":26614,"InitTime":1778955432808,"entry_fee":21,"match_id":32,"instance_started":true,"rc_prize":34,"your_team":"red"},"for_blue":{"Players":[{"Name":"warlock2","LastSeen":1778955432808,"UserId":3,"l10_wins":1,"l10_losses":3},{"Name":"warlock","LastSeen":1778955433214,"UserId":2,"l10_wins":3,"l10_losses":1}],"GameName":"soccar","Port":26614,"InitTime":1778955432808,"entry_fee":21,"match_id":32,"instance_started":true,"rc_prize":34,"your_team":"blue"}} [8/27/2026 1:43:53 PM] Still waiting: {"ok":true,"entry_fee":21,"rc_prize":34,"for_red":{"Players":[{"Name":"warlock","LastSeen":1787818433058,"UserId":2,"l10_wins":1,"l10_losses":8}],"GameName":"soccar","Port":26357,"InitTime":1787818433058,"entry_fee":21,"match_id":595,"rc_prize":34,"your_team":"red"},"for_blue":null}
[5/16/2026 11:47:13 PM] Configured dedicated match reporting for match id 32 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com [8/27/2026 1:43:53 PM] Got into a room
[5/16/2026 11:47:13 PM] Setting cupid to load game scene [8/27/2026 1:43:53 PM] {"ok":true,"entry_fee":21,"rc_prize":34,"for_red":{"Players":[{"Name":"warlock","LastSeen":1787818433058,"UserId":2,"l10_wins":1,"l10_losses":8},{"Name":"warlock2","LastSeen":1787818433141,"UserId":3,"l10_wins":8,"l10_losses":2}],"GameName":"soccar","Port":26357,"InitTime":1787818433058,"entry_fee":21,"match_id":595,"instance_started":true,"rc_prize":34,"your_team":"red"},"for_blue":{"Players":[{"Name":"warlock","LastSeen":1787818433058,"UserId":2,"l10_wins":1,"l10_losses":8},{"Name":"warlock2","LastSeen":1787818433141,"UserId":3,"l10_wins":8,"l10_losses":2}],"GameName":"soccar","Port":26357,"InitTime":1787818433058,"entry_fee":21,"match_id":595,"instance_started":true,"rc_prize":34,"your_team":"blue"}}
[5/16/2026 11:47:13 PM] Loading game scene [8/27/2026 1:43:53 PM] Configured dedicated match reporting for match id 595 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[5/16/2026 11:47:17 PM] Starting client at $kickkingsapi.playpoolstudios.com:26614 [8/27/2026 1:43:53 PM] Setting cupid to load game scene
[5/16/2026 11:47:18 PM] Client connected [8/27/2026 1:43:53 PM] Loading game scene
[5/16/2026 11:47:26 PM] Selected team changed from Red to Blue [8/27/2026 1:43:54 PM] Got into a room
[5/16/2026 11:47:30 PM] Score changed from 0 to 1 [8/27/2026 1:43:54 PM] {"ok":true,"entry_fee":21,"rc_prize":34,"for_red":{"Players":[{"Name":"warlock","LastSeen":1787818434370,"UserId":2,"l10_wins":1,"l10_losses":8},{"Name":"warlock2","LastSeen":1787818433141,"UserId":3,"l10_wins":8,"l10_losses":2}],"GameName":"soccar","Port":26357,"InitTime":1787818433058,"entry_fee":21,"match_id":595,"instance_started":true,"rc_prize":34,"your_team":"red"},"for_blue":{"Players":[{"Name":"warlock","LastSeen":1787818434370,"UserId":2,"l10_wins":1,"l10_losses":8},{"Name":"warlock2","LastSeen":1787818433141,"UserId":3,"l10_wins":8,"l10_losses":2}],"GameName":"soccar","Port":26357,"InitTime":1787818433058,"entry_fee":21,"match_id":595,"instance_started":true,"rc_prize":34,"your_team":"blue"}}
[5/16/2026 11:47:33 PM] Selected team changed from Blue to Red [8/27/2026 1:43:54 PM] Configured dedicated match reporting for match id 595 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[5/16/2026 11:47:39 PM] Selected team changed from Red to Blue [8/27/2026 1:43:54 PM] Setting cupid to load game scene
[5/16/2026 11:47:43 PM] Selected team changed from Blue to Red [8/27/2026 1:43:54 PM] Loading game scene
[5/16/2026 11:47:48 PM] Selected team changed from Red to Blue [8/27/2026 1:43:59 PM] Starting client at $kkapi.playpoolstudios.com:26357
[5/16/2026 11:47:50 PM] Score changed from 1 to 2 [8/27/2026 1:43:59 PM] Client connected
[5/16/2026 11:47:53 PM] Selected team changed from Blue to Red [8/27/2026 1:43:59 PM] Dedicated join: team assigned Blue isServer=False matchId=595 secretEmpty=False redJoin=False blueJoin=False collected=False
[5/16/2026 11:47:57 PM] Selected team changed from Red to Blue [8/27/2026 1:44:01 PM] Starting client at $kkapi.playpoolstudios.com:26357
[5/16/2026 11:48:02 PM] Score changed from 2 to 3 [8/27/2026 1:44:01 PM] Client connected
[5/16/2026 11:48:14 PM] Client disconnected [8/27/2026 1:44:01 PM] Dedicated join: team assigned Red isServer=False matchId=595 secretEmpty=False redJoin=False blueJoin=False collected=False
[5/16/2026 11:48:18 PM] Cupid settings updated [8/27/2026 1:44:17 PM] Selected team changed from Red to Blue
[5/16/2026 11:48:18 PM] Cupid settings updated [8/27/2026 1:44:17 PM] Selected team changed from Red to Blue
[8/27/2026 1:44:26 PM] Selected team changed from Blue to Red
[8/27/2026 1:44:26 PM] Selected team changed from Blue to Red
[8/27/2026 1:45:01 PM] Configured dedicated match reporting for match id 596 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[8/27/2026 1:45:01 PM] Client disconnected
[8/27/2026 1:45:01 PM] Client disconnected
[8/27/2026 1:45:05 PM] Starting client at $kkapi.playpoolstudios.com:26411
[8/27/2026 1:45:05 PM] Client connected
[8/27/2026 1:45:05 PM] Dedicated join: team assigned Red isServer=False matchId=596 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/27/2026 1:45:31 PM] Client disconnected
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d5d4b6e75e17172409ca64a1f41951a1
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4590b1d665d10c64a8174323d734b971
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+11
View File
@@ -0,0 +1,11 @@
[8/13/2026 7:50:10 PM] ReplayRecorder started (practice) id -1786630810 with 11 entities
[8/13/2026 7:50:10 PM] Client connected
[8/13/2026 7:50:10 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/13/2026 7:50:10 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/13/2026 7:50:10 PM] Selected team changed from Red to Blue
[8/13/2026 7:50:27 PM] Match: Blue skipped turn (1/2)
[8/13/2026 7:50:27 PM] Selected team changed from Blue to Red
[8/13/2026 7:50:29 PM] Dedicated match: Blue disconnected (order count=1)
[8/13/2026 7:50:29 PM] Mirror server: client disconnected (connId=0)
[8/13/2026 7:50:29 PM] Client disconnected
[8/13/2026 7:50:29 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1786630810.json (951 frames, 0 events, duration 19.02s)
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 13e09da064f35f74b83f6ed19880c04d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 084e9b7d2b988a34eb1c0aed4752ee16
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+17
View File
@@ -0,0 +1,17 @@
[8/13/2026 8:43:35 PM] ReplayRecorder started (practice) id -1786634015 with 11 entities
[8/13/2026 8:43:35 PM] Client connected
[8/13/2026 8:43:35 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/13/2026 8:43:35 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/13/2026 8:43:35 PM] Selected team changed from Red to Blue
[8/13/2026 8:43:52 PM] Match: Blue skipped turn (1/2)
[8/13/2026 8:43:52 PM] Selected team changed from Blue to Red
[8/13/2026 8:43:55 PM] AI best puck Puck: total=3.04 (prox=0.85, between=0.03, traj=0.70, push=1.00)
[8/13/2026 8:43:55 PM] force = 70 * 0.9010412 = 63.07288
[8/13/2026 8:43:55 PM] launching puck at (0.00, -4.00) with (0.00, 252.29) force
[8/13/2026 8:43:58 PM] Selected team changed from Red to Blue
[8/13/2026 8:44:05 PM] force = 70 * 0.9955804 = 69.69063
[8/13/2026 8:44:05 PM] launching puck at (-0.77, 4.94) with (53.42, -344.33) force
[8/13/2026 8:44:08 PM] Dedicated match: Blue disconnected (order count=1)
[8/13/2026 8:44:08 PM] Mirror server: client disconnected (connId=0)
[8/13/2026 8:44:08 PM] Client disconnected
[8/13/2026 8:44:08 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1786634015.json (1638 frames, 6 events, duration 32.76s)
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a085b5292682b144dacde97500667411
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 48b08b67d9b52ad4c9484786cf2ec2e1
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
[8/13/2026 8:49:52 PM] ReplayRecorder started (practice) id -1786634392 with 11 entities
[8/13/2026 8:49:52 PM] Client connected
[8/13/2026 8:49:53 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/13/2026 8:49:53 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/13/2026 8:49:53 PM] Selected team changed from Red to Blue
[8/13/2026 8:50:09 PM] Selected team changed from Blue to Red
[8/13/2026 8:50:12 PM] AI best puck Puck: total=3.04 (prox=0.85, between=0.03, traj=0.70, push=1.00)
[8/13/2026 8:50:12 PM] force = 70 * 0.9010412 = 63.07288
[8/13/2026 8:50:12 PM] launching puck at (0.00, -4.00) with (0.00, 252.29) force
[8/13/2026 8:50:15 PM] Selected team changed from Red to Blue
[8/13/2026 8:50:33 PM] Selected team changed from Blue to Red
[8/13/2026 8:50:35 PM] AI best puck Puck: total=1.73 (prox=0.93, between=0.03, traj=0.00, push=1.00)
[8/13/2026 8:50:35 PM] force = 70 * 0.9010412 = 63.07288
[8/13/2026 8:50:35 PM] launching puck at (0.00, -4.00) with (0.00, 252.29) force
[8/13/2026 8:50:38 PM] Selected team changed from Red to Blue
[8/13/2026 8:50:53 PM] Selected team changed from Blue to Red
[8/13/2026 8:50:55 PM] AI best puck Puck: total=1.73 (prox=0.94, between=0.03, traj=0.00, push=1.00)
[8/13/2026 8:50:55 PM] force = 70 * 0.9010412 = 63.07288
[8/13/2026 8:50:55 PM] launching puck at (0.00, -4.00) with (0.00, 252.29) force
[8/13/2026 8:50:57 PM] Selected team changed from Red to Blue
[8/13/2026 8:51:00 PM] Dedicated match: Blue disconnected (order count=1)
[8/13/2026 8:51:00 PM] Mirror server: client disconnected (connId=0)
[8/13/2026 8:51:00 PM] Client disconnected
[8/13/2026 8:51:00 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1786634392.json (3373 frames, 15 events, duration 67.46s)
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ecdf112848437a94b8bc3ad0bb197473
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 312a453df6e098149af89b3a2094beb5
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
[8/20/2026 7:19:02 PM] ReplayRecorder started (practice) id -1787233742 with 11 entities
[8/20/2026 7:19:02 PM] Client connected
[8/20/2026 7:19:03 PM] Dedicated join: team assigned Red isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/20/2026 7:19:03 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/20/2026 7:19:19 PM] Selected team changed from Red to Blue
[8/20/2026 7:19:22 PM] AI best puck Puck (7): total=3.04 (prox=0.85, between=0.03, traj=0.70, push=1.00)
[8/20/2026 7:19:22 PM] force = 70 * 0.9010412 = 63.07288
[8/20/2026 7:19:22 PM] launching puck at (0.00, 4.00) with (0.00, -252.29) force
[8/20/2026 7:19:25 PM] Selected team changed from Blue to Red
[8/20/2026 7:19:41 PM] Selected team changed from Red to Blue
[8/20/2026 7:19:44 PM] AI best puck Puck (7): total=1.73 (prox=0.93, between=0.03, traj=0.00, push=1.00)
[8/20/2026 7:19:44 PM] force = 70 * 0.9010412 = 63.07288
[8/20/2026 7:19:44 PM] launching puck at (0.00, 4.00) with (0.00, -252.29) force
[8/20/2026 7:19:47 PM] Selected team changed from Blue to Red
[8/20/2026 7:20:02 PM] Selected team changed from Red to Blue
[8/20/2026 7:20:05 PM] AI best puck Puck (7): total=1.85 (prox=0.94, between=0.11, traj=0.00, push=1.00)
[8/20/2026 7:20:05 PM] force = 70 * 0.9010412 = 63.07288
[8/20/2026 7:20:05 PM] launching puck at (0.00, 4.00) with (0.00, -252.29) force
[8/20/2026 7:20:05 PM] Score changed from 0 to 1
[8/20/2026 7:20:05 PM] Blue goal scored, blue score is now 1
[8/20/2026 7:20:05 PM] Dedicated match: Red disconnected (order count=1)
[8/20/2026 7:20:05 PM] Mirror server: client disconnected (connId=0)
[8/20/2026 7:20:05 PM] Client disconnected
[8/20/2026 7:20:05 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1787233742.json (3127 frames, 12 events, duration 62.54s)
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e3ae7f36bed6c134797b7cc9a0ea5f22
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2cc4ee68f3294624c9898f706bab87a8
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
[8/26/2026 10:21:50 PM] ReplayRecorder started (practice) id -1787763110 with 11 entities
[8/26/2026 10:21:50 PM] Client connected
[8/26/2026 10:21:51 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/26/2026 10:21:51 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/26/2026 10:21:51 PM] Selected team changed from Red to Blue
[8/26/2026 10:21:55 PM] Match: Blue forfeited by leave — Red wins
[8/26/2026 10:21:55 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1787763110.json (218 frames, 0 events, duration 4.36s)
[8/26/2026 10:21:55 PM] Dedicated match: end winner=Red red_connected=False blue_connected=False red_score=0 blue_score=0 forfeit=True
[8/26/2026 10:21:55 PM] Mirror server: client disconnected (connId=0)
[8/26/2026 10:21:55 PM] Client disconnected
[8/26/2026 10:22:00 PM] Cupid settings updated
[8/26/2026 10:22:01 PM] Cupid settings updated
[8/26/2026 10:22:03 PM] Configured dedicated match reporting for match id 0 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[8/26/2026 10:22:10 PM] Invalid Room port
[8/26/2026 10:22:10 PM] Mirror server exception logging enabled
[8/26/2026 10:22:10 PM] Game started On localhost, tutorial mode enabled
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: eafec47de336f534da962bda68ab7dfb
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 62574c44b8b7522459dc1aebd641aa7f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
[8/26/2026 10:22:10 PM] ReplayRecorder started (practice) id -1787763130 with 11 entities
[8/26/2026 10:22:10 PM] Client connected
[8/26/2026 10:22:10 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/26/2026 10:22:10 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/26/2026 10:22:10 PM] Selected team changed from Red to Blue
[8/26/2026 10:22:14 PM] Match: Blue forfeited by leave — Red wins
[8/26/2026 10:22:14 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1787763130.json (198 frames, 0 events, duration 3.96s)
[8/26/2026 10:22:14 PM] Dedicated match: end winner=Red red_connected=False blue_connected=False red_score=0 blue_score=0 forfeit=True
[8/26/2026 10:22:14 PM] Mirror server: client disconnected (connId=0)
[8/26/2026 10:22:14 PM] Client disconnected
[8/26/2026 10:22:19 PM] Cupid settings updated
[8/26/2026 10:22:19 PM] Cupid settings updated
[8/26/2026 10:22:22 PM] Configured dedicated match reporting for match id 0 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[8/26/2026 10:22:29 PM] Invalid Room port
[8/26/2026 10:22:29 PM] Mirror server exception logging enabled
[8/26/2026 10:22:29 PM] Game started On localhost, tutorial mode enabled
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6e233967e946ccc4b921379b66b22740
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5e168c91dfd19c748907045eef730143
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
[8/26/2026 10:22:29 PM] ReplayRecorder started (practice) id -1787763149 with 11 entities
[8/26/2026 10:22:29 PM] Client connected
[8/26/2026 10:22:29 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/26/2026 10:22:29 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/26/2026 10:22:29 PM] Selected team changed from Red to Blue
[8/26/2026 10:22:32 PM] Match: Blue forfeited by leave — Red wins
[8/26/2026 10:22:32 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1787763149.json (171 frames, 0 events, duration 3.42s)
[8/26/2026 10:22:32 PM] Dedicated match: end winner=Red red_connected=False blue_connected=False red_score=0 blue_score=0 forfeit=True
[8/26/2026 10:22:32 PM] Mirror server: client disconnected (connId=0)
[8/26/2026 10:22:32 PM] Client disconnected
[8/26/2026 10:22:36 PM] Cupid settings updated
[8/26/2026 10:22:37 PM] Cupid settings updated
[8/26/2026 10:22:38 PM] Configured dedicated match reporting for match id 0 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[8/26/2026 10:22:45 PM] Invalid Room port
[8/26/2026 10:22:45 PM] Mirror server exception logging enabled
[8/26/2026 10:22:45 PM] Game started On localhost, tutorial mode enabled
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: afd7819d04d262d42a80b619146a0acc
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f104f470c2a16344ea9b9aab39ff0f37
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
[8/26/2026 10:22:45 PM] ReplayRecorder started (practice) id -1787763165 with 11 entities
[8/26/2026 10:22:45 PM] Client connected
[8/26/2026 10:22:45 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/26/2026 10:22:45 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/26/2026 10:22:45 PM] Selected team changed from Red to Blue
[8/26/2026 10:22:57 PM] Match: Blue forfeited by leave — Red wins
[8/26/2026 10:22:57 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1787763165.json (605 frames, 0 events, duration 12.10s)
[8/26/2026 10:22:57 PM] Dedicated match: end winner=Red red_connected=False blue_connected=False red_score=0 blue_score=0 forfeit=True
[8/26/2026 10:22:57 PM] Mirror server: client disconnected (connId=0)
[8/26/2026 10:22:57 PM] Client disconnected
[8/26/2026 10:23:02 PM] Cupid settings updated
[8/26/2026 10:23:02 PM] Cupid settings updated
[8/26/2026 10:23:05 PM] Configured dedicated match reporting for match id 0 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[8/26/2026 10:23:11 PM] Invalid Room port
[8/26/2026 10:23:11 PM] Mirror server exception logging enabled
[8/26/2026 10:23:11 PM] Game started On localhost, tutorial mode enabled
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b6b634e5c0a824d47b49e89290032735
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+1
View File
@@ -0,0 +1 @@
{"version":1,"matchId":-1787763191,"isPractice":true,"fixedDeltaTime":0.019999992102384568,"duration":0.0,"entities":[{"id":0,"type":"ball","team":""},{"id":1,"type":"puck","team":"Red"},{"id":2,"type":"puck","team":"Red"},{"id":3,"type":"puck","team":"Red"},{"id":4,"type":"puck","team":"Red"},{"id":5,"type":"puck","team":"Red"},{"id":6,"type":"puck","team":"Blue"},{"id":7,"type":"puck","team":"Blue"},{"id":8,"type":"puck","team":"Blue"},{"id":9,"type":"puck","team":"Blue"},{"id":10,"type":"puck","team":"Blue"}],"frames":[],"events":[]}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d1dd94f034d5fcc48a72276d45f82c83
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
[8/26/2026 10:23:11 PM] ReplayRecorder started (practice) id -1787763191 with 11 entities
[8/26/2026 10:23:11 PM] Client connected
[8/26/2026 10:23:11 PM] Dedicated join: team assigned Blue isServer=True matchId=0 secretEmpty=False redJoin=False blueJoin=False collected=False
[8/26/2026 10:23:11 PM] Dedicated join: skipping PATCH (matchId=0, secretEmpty=False)
[8/26/2026 10:23:11 PM] Dedicated match: Blue disconnected (order count=1)
[8/26/2026 10:23:11 PM] Mirror server: client disconnected (connId=0)
[8/26/2026 10:23:11 PM] Client disconnected
[8/26/2026 10:23:11 PM] ReplayRecorder wrote F:/Projects/Unity/soccar2d/Assets\Logs\-1787763191.json (0 frames, 0 events, duration 0.00s)
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 13d3b05348f90644b8f236f835efa4c2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+37
View File
@@ -411,3 +411,40 @@
[5/1/2026 1:13:06 AM] Active player connections: 0 [5/1/2026 1:13:06 AM] Active player connections: 0
[5/1/2026 1:13:12 AM] Server will be closed due to no players in, 40 [5/1/2026 1:13:12 AM] Server will be closed due to no players in, 40
[5/1/2026 1:13:22 AM] Server will be closed due to no players in, 30 [5/1/2026 1:13:22 AM] Server will be closed due to no players in, 30
[7/19/2026 12:42:42 AM] Starting server at port 7777
[7/19/2026 12:42:42 AM] Mirror server exception logging enabled
[7/19/2026 12:42:42 AM] Failed to fetch red and blue names, isServer?
[7/19/2026 12:42:42 AM] Starting auto close watchdog
[7/19/2026 12:42:42 AM] Active player connections: 0
[8/13/2026 2:36:25 PM] Starting server at port 7777
[8/13/2026 2:36:25 PM] Mirror server exception logging enabled
[8/13/2026 2:36:25 PM] Failed to fetch red and blue names, isServer?
[8/13/2026 2:36:25 PM] Starting auto close watchdog
[8/13/2026 2:36:25 PM] Active player connections: 0
[8/13/2026 3:56:45 PM] Starting server at port 7777
[8/13/2026 3:56:45 PM] Mirror server exception logging enabled
[8/13/2026 3:56:45 PM] Failed to fetch red and blue names, isServer?
[8/13/2026 3:56:45 PM] Starting auto close watchdog
[8/13/2026 3:56:45 PM] Active player connections: 0
[8/13/2026 3:56:52 PM] Server will be closed due to no players in, 60
[8/13/2026 9:06:10 PM] Starting server at port 7777
[8/13/2026 9:06:10 PM] Mirror server exception logging enabled
[8/13/2026 9:06:10 PM] Failed to fetch red and blue names, isServer?
[8/13/2026 9:06:10 PM] Starting auto close watchdog
[8/13/2026 9:06:10 PM] Active player connections: 0
[8/18/2026 2:02:56 AM] Starting server at port 7777
[8/18/2026 2:02:56 AM] Mirror server exception logging enabled
[8/18/2026 2:02:56 AM] Failed to fetch red and blue names, isServer?
[8/18/2026 2:02:56 AM] Starting auto close watchdog
[8/18/2026 2:02:56 AM] Active player connections: 0
[8/27/2026 1:12:53 PM] Starting server at port 7777
[8/27/2026 1:12:53 PM] Mirror server exception logging enabled
[8/27/2026 1:12:53 PM] Failed to fetch red and blue names, isServer?
[8/27/2026 1:12:53 PM] Starting auto close watchdog
[8/27/2026 1:12:53 PM] Active player connections: 0
[8/27/2026 1:12:59 PM] Server will be closed due to no players in, 60
[8/27/2026 1:42:51 PM] Starting server at port 7777
[8/27/2026 1:42:51 PM] Mirror server exception logging enabled
[8/27/2026 1:42:51 PM] Failed to fetch red and blue names, isServer?
[8/27/2026 1:42:51 PM] Starting auto close watchdog
[8/27/2026 1:42:51 PM] Active player connections: 0
+23 -23
View File
@@ -578,7 +578,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!224 &4426299922493186842 --- !u!224 &4426299922493186842
RectTransform: RectTransform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -595,7 +595,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -12, y: -301.3} m_AnchoredPosition: {x: -12, y: -289}
m_SizeDelta: {x: 396.3171, y: 206.1671} m_SizeDelta: {x: 396.3171, y: 206.1671}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5777954290886970586 --- !u!222 &5777954290886970586
@@ -619,7 +619,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 0.8349056, g: 0.93029827, b: 1, a: 1}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -671,7 +671,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 351.29, y: 619.50073} m_AnchoredPosition: {x: 291.99997, y: 619.50073}
m_SizeDelta: {x: 356.21, y: 79.8881} m_SizeDelta: {x: 356.21, y: 79.8881}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4299910807936819090 --- !u!222 &4299910807936819090
@@ -747,7 +747,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_Sprite: {fileID: -5110294184840258280, guid: b3993aed15d744d4bb390d5351cda423, type: 3} m_Sprite: {fileID: 0}
m_Type: 0 m_Type: 0
m_PreserveAspect: 0 m_PreserveAspect: 0
m_FillCenter: 1 m_FillCenter: 1
@@ -789,9 +789,9 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 784446025213843339} m_Father: {fileID: 784446025213843339}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 1} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 349.8392, y: -415.53} m_AnchoredPosition: {x: 349.8392, y: 714.47}
m_SizeDelta: {x: 287.9087, y: 53.690674} m_SizeDelta: {x: 287.9087, y: 53.690674}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4680061499564276536 --- !u!222 &4680061499564276536
@@ -928,7 +928,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -351.29, y: 619.50073} m_AnchoredPosition: {x: -297.8, y: 619.50073}
m_SizeDelta: {x: 356.21, y: 79.8881} m_SizeDelta: {x: 356.21, y: 79.8881}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2120176914018994436 --- !u!222 &2120176914018994436
@@ -1198,8 +1198,8 @@ MonoBehaviour:
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 4294967295
m_fontSize: 49.1 m_fontSize: 46.1
m_fontSizeBase: 49.1 m_fontSizeBase: 46.1
m_fontWeight: 400 m_fontWeight: 400
m_enableAutoSizing: 0 m_enableAutoSizing: 0
m_fontSizeMin: 18 m_fontSizeMin: 18
@@ -1277,7 +1277,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -351.29, y: 619.50073} m_AnchoredPosition: {x: -297.8, y: 619.50073}
m_SizeDelta: {x: 356.21, y: 79.8881} m_SizeDelta: {x: 356.21, y: 79.8881}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &588061042461009508 --- !u!222 &588061042461009508
@@ -1456,9 +1456,9 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 784446025213843339} m_Father: {fileID: 784446025213843339}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 1} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -349.83923, y: -415.53} m_AnchoredPosition: {x: -349.83923, y: 714.47}
m_SizeDelta: {x: 287.9087, y: 53.690674} m_SizeDelta: {x: 287.9087, y: 53.690674}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4305625922513859524 --- !u!222 &4305625922513859524
@@ -1868,7 +1868,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -0.0000059605, y: 619.1} m_AnchoredPosition: {x: -0.0000059605, y: 619.1}
m_SizeDelta: {x: 433.0019, y: 98.6273} m_SizeDelta: {x: 359.3, y: 98.6273}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8725577243422270907 --- !u!222 &8725577243422270907
CanvasRenderer: CanvasRenderer:
@@ -2094,7 +2094,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -6.8129, y: 711.5879} m_AnchoredPosition: {x: -6.8129, y: 711.5879}
m_SizeDelta: {x: 370.1125, y: 86.3439} m_SizeDelta: {x: 267.8, y: 86.3439}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2040376918808009174 --- !u!222 &2040376918808009174
CanvasRenderer: CanvasRenderer:
@@ -2244,7 +2244,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 351.29, y: 619.50073} m_AnchoredPosition: {x: 292, y: 619.50073}
m_SizeDelta: {x: 356.21, y: 79.8881} m_SizeDelta: {x: 356.21, y: 79.8881}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3873575104506013387 --- !u!222 &3873575104506013387
@@ -2470,7 +2470,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_Sprite: {fileID: 8390053243822926539, guid: 511a187332d4a4d4db6f55f623e1de11, type: 3} m_Sprite: {fileID: 8390053243822926539, guid: 5b969ed0d82676d4890df6c74fd6f3c4, type: 3}
m_Type: 0 m_Type: 0
m_PreserveAspect: 0 m_PreserveAspect: 0
m_FillCenter: 1 m_FillCenter: 1
@@ -2728,7 +2728,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_Sprite: {fileID: 8390053243822926539, guid: 511a187332d4a4d4db6f55f623e1de11, type: 3} m_Sprite: {fileID: 8390053243822926539, guid: 5b969ed0d82676d4890df6c74fd6f3c4, type: 3}
m_Type: 0 m_Type: 0
m_PreserveAspect: 0 m_PreserveAspect: 0
m_FillCenter: 1 m_FillCenter: 1
@@ -3625,7 +3625,7 @@ MonoBehaviour:
p2_name: {fileID: 6139878670004169448} p2_name: {fileID: 6139878670004169448}
p1_l10: {fileID: 7658668847612786734} p1_l10: {fileID: 7658668847612786734}
p2_l10: {fileID: 8415686484963452604} p2_l10: {fileID: 8415686484963452604}
redIcon: {fileID: 7194123030578827194, guid: 809c8d780dc80f14e80fd92c6e42ed71, type: 3} redIcon: {fileID: 7609527126711406816, guid: 532bcbd7fabd63844ac75765ae25dcb9, type: 3}
blueIcon: {fileID: 7609527126711406816, guid: 8d015c6e4a5ab8540814b06ae5f85980, type: 3} blueIcon: {fileID: 7609527126711406816, guid: 8d015c6e4a5ab8540814b06ae5f85980, type: 3}
txtMiddle: {fileID: 2458004521057696282} txtMiddle: {fileID: 2458004521057696282}
txtRcPrize: {fileID: 8330661879995437874} txtRcPrize: {fileID: 8330661879995437874}
@@ -3953,7 +3953,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1} m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 1.326294, y: -53.300316} m_AnchoredPosition: {x: 1.326294, y: -53.300537}
m_SizeDelta: {x: 82.232544, y: 5.79937} m_SizeDelta: {x: 82.232544, y: 5.79937}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8345919255487113005 --- !u!222 &8345919255487113005
@@ -3984,7 +3984,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_Sprite: {fileID: -7443756053782179069, guid: 0ec6ee71c3b91214284f88b794c2f1e7, type: 3} m_Sprite: {fileID: -1955448553306655712, guid: b9f492da50bba6a4cae145945abc32e8, type: 3}
m_Type: 0 m_Type: 0
m_PreserveAspect: 0 m_PreserveAspect: 0
m_FillCenter: 1 m_FillCenter: 1
+4 -34
View File
@@ -14,8 +14,6 @@ GameObject:
- component: {fileID: 9033708766278989701} - component: {fileID: 9033708766278989701}
- component: {fileID: 2872566638177417579} - component: {fileID: 2872566638177417579}
- component: {fileID: 407622981302802273} - component: {fileID: 407622981302802273}
- component: {fileID: 969959851011469098}
- component: {fileID: 2978779054327924816}
m_Layer: 5 m_Layer: 5
m_Name: text_emote_prefab m_Name: text_emote_prefab
m_TagString: Untagged m_TagString: Untagged
@@ -165,34 +163,6 @@ MonoBehaviour:
m_OnClick: m_OnClick:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
--- !u!114 &969959851011469098
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4492919853174754762}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0b148fe25e99eb48b9724523833bab1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Delegates: []
--- !u!114 &2978779054327924816
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4492919853174754762}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e4c65221cd965e45853c892a9facd82, type: 3}
m_Name:
m_EditorClassIdentifier:
scaleFactor: 1.1
duration: 0.1
ease: 27
--- !u!1 &4797427416459394329 --- !u!1 &4797427416459394329
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -225,10 +195,10 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 1018686033926137723} m_Father: {fileID: 1018686033926137723}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1} m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 78.775, y: -32.235} m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 137.55, y: 44.47} m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &101741276978323686 --- !u!222 &101741276978323686
CanvasRenderer: CanvasRenderer:
+7314 -278
View File
File diff suppressed because it is too large Load Diff
+4628 -43
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c886581a7bac8f34ba8f7995eb48db02
+58 -3
View File
@@ -15,22 +15,73 @@ public class Ball : NetworkBehaviour
void Awake() void Awake()
{ {
rb = GetComponent<Rigidbody2D>(); rb = GetComponent<Rigidbody2D>();
col = GetComponent<Collider2D>();
startPosition = transform.position; startPosition = transform.position;
} }
Collider2D col;
Coroutine coroutineReset;
bool holdingCollisionLock;
public void SetCollisionsEnabled(bool enabled)
{
if (col != null)
col.enabled = enabled;
if (rb != null && !enabled)
{
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
}
}
public void Reset(float duration){ public void Reset(float duration){
StartCoroutine(CoroutineReset(duration)); if (coroutineReset != null)
{
StopCoroutine(coroutineReset);
RestoreAfterReset();
}
coroutineReset = StartCoroutine(CoroutineReset(duration));
} }
IEnumerator CoroutineReset(float duration){ IEnumerator CoroutineReset(float duration){
if (GameManager.instance != null)
{
GameManager.instance.PushResetCollisionLock();
holdingCollisionLock = true;
}
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
float t=0; float t=0;
Vector3 pos1 = transform.position; Vector3 pos1 = transform.position;
Quaternion rot1 = transform.rotation; Quaternion rot1 = transform.rotation;
while (t < duration){ while (t < duration){
t+=Time.deltaTime; t+=Time.deltaTime;
transform.position = Vector3.Lerp(pos1, startPosition, t / duration); float u = Mathf.Clamp01(t / duration);
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, t/duration); transform.position = Vector3.Lerp(pos1, startPosition, u);
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
yield return null; yield return null;
} }
transform.position = startPosition;
transform.rotation = Quaternion.identity;
RestoreAfterReset();
}
void RestoreAfterReset()
{
if (rb != null)
{
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
}
if (holdingCollisionLock && GameManager.instance != null)
{
GameManager.instance.PopResetCollisionLock();
holdingCollisionLock = false;
}
coroutineReset = null;
} }
public bool touchingB,touchingL,touchingR,touchingT = false; public bool touchingB,touchingL,touchingR,touchingT = false;
@@ -96,10 +147,14 @@ public class Ball : NetworkBehaviour
if(collision.collider.CompareTag("wall")){ if(collision.collider.CompareTag("wall")){
AudioManager.instance.PlayWallHit(vol); AudioManager.instance.PlayWallHit(vol);
if (isServer)
ReplayRecorder.RecordHit("ball_wall", vol, this);
RpcWallHitAudio(vol); RpcWallHitAudio(vol);
} }
else if(collision.collider.CompareTag("Puck")){ else if(collision.collider.CompareTag("Puck")){
AudioManager.instance.PlayBallHit(vol); AudioManager.instance.PlayBallHit(vol);
if (isServer)
ReplayRecorder.RecordHit("ball_puck", vol, this);
RpcBallHitAudio(vol); RpcBallHitAudio(vol);
} }
+70 -3
View File
@@ -22,6 +22,7 @@ public class GameCanvas : MonoBehaviour
ShowWaitingForOpponentPanel(); ShowWaitingForOpponentPanel();
HideEmotesPanel(); HideEmotesPanel();
HideAllPostGamePanels();
} }
void OnDestroy() void OnDestroy()
@@ -59,6 +60,10 @@ public class GameCanvas : MonoBehaviour
public float emojiEmoteDuration = 5f; public float emojiEmoteDuration = 5f;
bool isShowingEmotes=> emotesPanel.gameObject.activeSelf; bool isShowingEmotes=> emotesPanel.gameObject.activeSelf;
public RectTransform kickWarning;
public GameObject[] postGamePanels;
public TMP_Text txtMatchNumber;
const float PostGamePanelDuration = 3f;
void Start(){ void Start(){
@@ -91,6 +96,9 @@ public class GameCanvas : MonoBehaviour
if (txtRcPrize != null) if (txtRcPrize != null)
txtRcPrize.text = CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC"; txtRcPrize.text = CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC";
if (txtMatchNumber != null && GameManager.DedicatedMatchId > 0)
txtMatchNumber.text = "M" + GameManager.DedicatedMatchId;
} }
void OnEmoteTextPressed(string txtName){ void OnEmoteTextPressed(string txtName){
@@ -164,17 +172,36 @@ public class GameCanvas : MonoBehaviour
} }
void OnLeaveGamePressed() void OnLeaveGamePressed()
{
if (MessageBoxDialog.IsAvailable)
{
MessageBoxDialog.Show(
"Forfeit match?",
"Leaving will result in a loss, making opponent the winner. Are you sure?",
confirmed =>
{
if (confirmed)
ConfirmLeaveMatch();
});
return;
}
ConfirmLeaveMatch();
}
void ConfirmLeaveMatch()
{ {
if (CupidLobby.instance != null) if (CupidLobby.instance != null)
CupidLobby.instance.Cancel(); CupidLobby.instance.Cancel();
LoginManager.ClearMatchYourTeam(); LoginManager.ClearMatchYourTeam();
if (NetworkManager.singleton != null && NetworkClient.active) if (GameManager.instance != null)
{ {
NetManager.MarkIntentionalClientDisconnect(); GameManager.instance.Leave();
NetworkManager.singleton.StopClient(); return;
} }
NetManager.StopNetworkingForSceneChange();
LevelLoadManager.LoadLevel("MainMenu"); LevelLoadManager.LoadLevel("MainMenu");
} }
@@ -303,4 +330,44 @@ public class GameCanvas : MonoBehaviour
timeoutForOpponentPanel.SetActive(false); timeoutForOpponentPanel.SetActive(false);
} }
public void ShowKickWarning(){
kickWarning.transform.localScale = new Vector3(0, 0, 0);
kickWarning.transform.DOScale(1, 0.2f).SetEase(Ease.OutBack);
kickWarning.gameObject.SetActive(true);
}
public void HideKickWarning(){
kickWarning.transform.DOScale(0, 0.2f).SetEase(Ease.InBack).OnComplete(() => {
kickWarning.gameObject.SetActive(false);
});
}
public IEnumerator ShowRandomPostGamePanelThenHide()
{
HideAllPostGamePanels();
if (postGamePanels == null || postGamePanels.Length == 0)
yield break;
GameObject chosen = postGamePanels[Random.Range(0, postGamePanels.Length)];
if (chosen == null)
yield break;
chosen.SetActive(true);
yield return new WaitForSeconds(PostGamePanelDuration);
chosen.SetActive(false);
}
void HideAllPostGamePanels()
{
if (postGamePanels == null)
return;
for (int i = 0; i < postGamePanels.Length; i++)
{
if (postGamePanels[i] != null)
postGamePanels[i].SetActive(false);
}
}
} }
File diff suppressed because it is too large Load Diff
+252 -20
View File
@@ -42,6 +42,25 @@ public class GameOverCanvas : MonoBehaviour
public float returnToMenuCountdown = 15f; public float returnToMenuCountdown = 15f;
/// <summary>Each player needs at least 2.0 RC (20 coin units) to rematch.</summary>
const int MinimumRematchRcCoins = 20;
[Header("Bet Limit")]
public TMP_Text txtRcBalance;
public TMP_Text txtBetLimitWarning;
public CanvasGroup betLimitAdjustPanel;
//minVal: 2, max Val 500, when its 500, show ∞ on the text
public Slider betLimitSlider;
public TMP_Text txtBetLimitSliderValue;
public Button btnBetLimitAdjustAccept;
public Button btnBetLimitAdjustCancel;
const string PersonalBetLimitPrefsKey = "WalkInvest.PersonalBetLimit";
const int BetLimitMin = 8;
const int BetLimitMax = 500;
const int BetLimitUnlimited = 500;
int rematchOpponentBetLimitCoins = BetLimitUnlimited;
[Header("Rematch panel")] [Header("Rematch panel")]
public CanvasGroup rematchPanel; public CanvasGroup rematchPanel;
@@ -83,6 +102,139 @@ public class GameOverCanvas : MonoBehaviour
btnConfirmBet.onClick.AddListener(OnBetAccepted); btnConfirmBet.onClick.AddListener(OnBetAccepted);
btnCancelBet.onClick.AddListener(OnBetCancelled); btnCancelBet.onClick.AddListener(OnBetCancelled);
SetupBetLimitUi();
}
void SetupBetLimitUi()
{
if (betLimitSlider != null)
{
betLimitSlider.minValue = BetLimitMin;
betLimitSlider.maxValue = BetLimitMax;
betLimitSlider.wholeNumbers = true;
betLimitSlider.onValueChanged.AddListener(OnBetLimitSliderValueChanged);
}
if (btnBetLimitAdjustAccept != null)
btnBetLimitAdjustAccept.onClick.AddListener(OnBetLimitAdjustAccepted);
if (btnBetLimitAdjustCancel != null)
btnBetLimitAdjustCancel.onClick.AddListener(CloseBetLimitAdjuster);
var warningButton = txtBetLimitWarning != null
? txtBetLimitWarning.GetComponentInChildren<Button>()
: null;
if (warningButton != null)
warningButton.onClick.AddListener(OpenBetLimitAdjuster);
}
public static int GetPersonalBetLimit()
{
return PlayerPrefs.GetInt(PersonalBetLimitPrefsKey, BetLimitUnlimited);
}
static int EffectiveBetLimitCap(int limitCoins) =>
limitCoins >= BetLimitUnlimited ? int.MaxValue : limitCoins;
int GetRematchMaxBetCoins()
{
int maxBet = Mathf.Min(
EffectiveBetLimitCap(rematchOpponentBetLimitCoins),
GetRcCoins(LevelLoadManager.myPlayer),
GetRcCoins(LevelLoadManager.opponentPlayer));
return Mathf.Max(0, maxBet);
}
void ApplyRematchBetSliderLimits()
{
if (betSlider == null)
return;
int maxBet = GetRematchMaxBetCoins();
betSlider.maxValue = maxBet;
if (maxBet <= 0)
{
betSlider.minValue = 0;
betSlider.value = 0;
}
else
{
betSlider.minValue = Mathf.Clamp(maxBet / 10f, 5, 10000);
betSlider.value = Mathf.Clamp(maxBet / 2, betSlider.minValue, maxBet);
}
OnBetSliderValueChanged(betSlider.value);
}
static void SetPersonalBetLimit(int limitCoins)
{
limitCoins = Mathf.Clamp(limitCoins, BetLimitMin, BetLimitMax);
PlayerPrefs.SetInt(PersonalBetLimitPrefsKey, limitCoins);
PlayerPrefs.Save();
}
static string FormatBetLimitLabel(int limitCoins)
{
if (limitCoins >= BetLimitUnlimited)
return "∞";
return CoinHelper.FormatString(limitCoins) + " RC";
}
static string FormatBetLimitWarningText(int limitCoins) =>
"Max Bet : " + FormatBetLimitLabel(limitCoins);
void UpdateBetLimitWarningText()
{
if (txtBetLimitWarning != null)
txtBetLimitWarning.text = FormatBetLimitWarningText(GetPersonalBetLimit());
}
void OnBetLimitSliderValueChanged(float value)
{
if (txtBetLimitSliderValue != null)
txtBetLimitSliderValue.text = FormatBetLimitLabel(Mathf.RoundToInt(value));
}
void ApplyBetLimitVisibility()
{
bool isLoser = myTeam != winningTeam;
if (txtBetLimitWarning != null)
txtBetLimitWarning.gameObject.SetActive(isLoser);
if (!isLoser)
return;
UpdateBetLimitWarningText();
}
public void OpenBetLimitAdjuster()
{
if (betLimitAdjustPanel == null)
return;
int limit = GetPersonalBetLimit();
if (betLimitSlider != null)
betLimitSlider.SetValueWithoutNotify(limit);
OnBetLimitSliderValueChanged(limit);
betLimitAdjustPanel.DOFade(1, 0.25f);
betLimitAdjustPanel.blocksRaycasts = true;
betLimitAdjustPanel.interactable = true;
}
public void CloseBetLimitAdjuster()
{
if (betLimitAdjustPanel == null)
return;
betLimitAdjustPanel.DOFade(0, 0.25f);
betLimitAdjustPanel.blocksRaycasts = false;
betLimitAdjustPanel.interactable = false;
}
void OnBetLimitAdjustAccepted()
{
if (betLimitSlider != null)
SetPersonalBetLimit(Mathf.RoundToInt(betLimitSlider.value));
UpdateBetLimitWarningText();
CloseBetLimitAdjuster();
} }
void OnBetAccepted(){ void OnBetAccepted(){
@@ -102,14 +254,18 @@ public class GameOverCanvas : MonoBehaviour
} }
void OnBtnRematchClicked(){ void OnBtnRematchClicked(){
// LevelLoadManager.LoadLevel("Game"); if (!BothPlayersHaveMinimumRematchRc())
return;
if (txtBetLimitWarning != null)
txtBetLimitWarning.gameObject.SetActive(false);
NetPlayer.localPlayer.RequestRematch(); NetPlayer.localPlayer.RequestRematch();
SetRematchPendingState(true); SetRematchPendingState(true);
} }
public void OnRematchRequested(){ public void OnRematchRequested(int opponentMaxBetLimitCoins){
rematchOpponentBetLimitCoins = opponentMaxBetLimitCoins;
if(winningTeam == myTeam){ if(winningTeam == myTeam){
//Only i can initiate the rematch, so here we go //Only the loser can initiate the rematch; winner sets the stake
ShowRematchPanel(); ShowRematchPanel();
}else{ }else{
rematchWarningLoser.text = "Waiting for opponent to accept"; rematchWarningLoser.text = "Waiting for opponent to accept";
@@ -129,7 +285,7 @@ public class GameOverCanvas : MonoBehaviour
void OnBtnLeaveGameClicked(){ void OnBtnLeaveGameClicked(){
GameManager.instance.Leave(); LevelLoadManager.instance.Leave();
} }
void CollectOriginalPositionsAndScales(){ void CollectOriginalPositionsAndScales(){
@@ -178,6 +334,13 @@ public class GameOverCanvas : MonoBehaviour
rematchPanel.alpha=0; rematchPanel.alpha=0;
rematchPanel.blocksRaycasts = false; rematchPanel.blocksRaycasts = false;
rematchPanel.interactable = false; rematchPanel.interactable = false;
if (betLimitAdjustPanel != null)
{
betLimitAdjustPanel.alpha = 0;
betLimitAdjustPanel.blocksRaycasts = false;
betLimitAdjustPanel.interactable = false;
}
} }
Team winningTeam; Team winningTeam;
@@ -217,6 +380,8 @@ public class GameOverCanvas : MonoBehaviour
p1_winner.gameObject.SetActive(false); p1_winner.gameObject.SetActive(false);
p2_winner.gameObject.SetActive(true); p2_winner.gameObject.SetActive(true);
} }
ApplyBetLimitVisibility();
} }
float rcPrize = 0; float rcPrize = 0;
@@ -246,16 +411,16 @@ public class GameOverCanvas : MonoBehaviour
item.localScale = new Vector3(0, 0, 0); item.localScale = new Vector3(0, 0, 0);
} }
if(winningTeam == myTeam){ if (GameTutorialManager.tutorialModeEnabled)
rcPrize = 0;
else if (winningTeam == myTeam)
rcPrize = CoinHelper.Format(GameManager.MatchRcPrizeCoins); rcPrize = CoinHelper.Format(GameManager.MatchRcPrizeCoins);
}
btnRematch.interactable = winningTeam != myTeam; if (btnRematch != null)
if(winningTeam == myTeam){ btnRematch.gameObject.SetActive(!GameTutorialManager.tutorialModeEnabled);
rematchWarningLoser.text = "Only the loser can request a rematch";
}else{ UpdateRematchAvailability();
rematchWarningLoser.text = ""; ApplyRcBalancesToUi(null);
}
StartCoroutine(CoroutineShow()); StartCoroutine(CoroutineShow());
@@ -290,11 +455,16 @@ public class GameOverCanvas : MonoBehaviour
yield return new WaitForSeconds(scaleTime * 0.5f); yield return new WaitForSeconds(scaleTime * 0.5f);
//Rewards // Rewards — +CC only if this client was actually awarded it (connected non-forfeit, or forfeit winner)
float ccReward = GameTutorialManager.tutorialModeEnabled || !GameManager.LocalPlayerEarnedParticipationCc
? 0f
: GameManager.ParticipationCcAmount;
StartCoroutine(CoroutineSetTextNumber(rcPrize, txtRewardRC, 10, "N1", "+", " RC")); StartCoroutine(CoroutineSetTextNumber(rcPrize, txtRewardRC, 10, "N1", "+", " RC"));
StartCoroutine(CoroutineSetTextNumber(100, txtRewardCC, 100, "N0", "+", " CC")); StartCoroutine(CoroutineSetTextNumber(ccReward, txtRewardCC, 100, "N0", "+", " CC"));
txtBetRC.text = CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC"; txtBetRC.text = GameTutorialManager.tutorialModeEnabled
? "0 RC"
: CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC";
yield return new WaitForSeconds(1f); yield return new WaitForSeconds(1f);
@@ -333,17 +503,14 @@ public class GameOverCanvas : MonoBehaviour
} }
GameManager.instance.Leave(); LevelLoadManager.instance.Leave();
} }
public void ShowRematchPanel(){ public void ShowRematchPanel(){
SetRematchPendingState(true); SetRematchPendingState(true);
ApplyRematchBetSliderLimits();
StartCoroutine(CoroutineShowRematchPanel()); StartCoroutine(CoroutineShowRematchPanel());
betSlider.maxValue = (LevelLoadManager.myPlayer.rc < LevelLoadManager.opponentPlayer.rc) ? LevelLoadManager.myPlayer.rc : LevelLoadManager.opponentPlayer.rc;
betSlider.value = betSlider.maxValue / 2;
betSlider.minValue = Mathf.Clamp(betSlider.maxValue /10f,5,10000);
} }
IEnumerator CoroutineShowRematchPanel(){ IEnumerator CoroutineShowRematchPanel(){
@@ -368,6 +535,22 @@ public class GameOverCanvas : MonoBehaviour
countdownCoroutine = StartCoroutine(CoroutineCountdown()); countdownCoroutine = StartCoroutine(CoroutineCountdown());
} }
public void OnRematchFailed(string errorMessage)
{
btnRematch.interactable = true;
btnLeave.interactable = true;
if (rematchWarningLoser != null)
rematchWarningLoser.text = "Rematch failed.";
StartCoroutine(CoroutineHideRematchPanel());
if (countdownCoroutine != null)
StopCoroutine(countdownCoroutine);
countdownCoroutine = StartCoroutine(CoroutineCountdown());
string body = string.IsNullOrEmpty(errorMessage) ? "Could not start a rematch." : errorMessage;
MessageBoxDialog.Show("Rematch failed", body);
}
IEnumerator CoroutineHideRematchPanel(){ IEnumerator CoroutineHideRematchPanel(){
rematchPopup.DOScale(0, 0.2f).SetEase(Ease.InBack); rematchPopup.DOScale(0, 0.2f).SetEase(Ease.InBack);
yield return new WaitForSeconds(0.2f); yield return new WaitForSeconds(0.2f);
@@ -406,6 +589,55 @@ public class GameOverCanvas : MonoBehaviour
txtMyRCBalance.text = myText; txtMyRCBalance.text = myText;
if (txtOpponentRCBalance != null) if (txtOpponentRCBalance != null)
txtOpponentRCBalance.text = oppText; txtOpponentRCBalance.text = oppText;
if (txtRcBalance != null)
txtRcBalance.text = loadingPlaceholder ?? (my != null ? CoinHelper.FormatString((int)my.rc) + " RC" : "0.0 RC");
if (loadingPlaceholder == null)
{
UpdateRematchAvailability();
if (rematchPanel != null && rematchPanel.alpha > 0.01f)
ApplyRematchBetSliderLimits();
}
}
static int GetRcCoins(MatchmadePlayer player) =>
player == null ? 0 : Mathf.Max(0, Mathf.FloorToInt(player.rc));
bool BothPlayersHaveMinimumRematchRc()
{
return GetRcCoins(LevelLoadManager.myPlayer) >= MinimumRematchRcCoins
&& GetRcCoins(LevelLoadManager.opponentPlayer) >= MinimumRematchRcCoins;
}
void UpdateRematchAvailability()
{
if (GameTutorialManager.tutorialModeEnabled)
{
btnRematch.interactable = false;
if (rematchWarningLoser != null)
rematchWarningLoser.text = "";
return;
}
if (winningTeam == myTeam)
{
btnRematch.interactable = false;
if (rematchWarningLoser != null)
rematchWarningLoser.text = "Only the loser can request a rematch";
return;
}
if (!BothPlayersHaveMinimumRematchRc())
{
btnRematch.interactable = false;
if (rematchWarningLoser != null)
rematchWarningLoser.text = "Not enough RC";
return;
}
btnRematch.interactable = true;
if (rematchWarningLoser != null)
rematchWarningLoser.text = "";
} }
IEnumerator FetchPlayerRcAndApply(string baseUrl, MatchmadePlayer player) IEnumerator FetchPlayerRcAndApply(string baseUrl, MatchmadePlayer player)
+364
View File
@@ -0,0 +1,364 @@
using System;
using System.Collections;
using Mirror;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using DG.Tweening;
public class GameTutorialManager : MonoBehaviour
{
public const string CpuOpponentName = "CPU";
public const string DefaultGameSceneName = "Game";
public const string CpuL10PrefsKey = "practice_cpu_l10";
const int L10Window = 10;
public static bool tutorialModeTrigger = false;
public bool isTutorial = false;
public static bool tutorialModeEnabled = false;
public AiBotController aiController;
[Header("UI")]
public CanvasGroup intro_shoot;
public CanvasGroup intro_goal;
public float introFadeDuration = 0.3f;
Button _introShootButton;
Button _introGoalButton;
bool _shootIntroDismissed;
bool _goalIntroDismissed;
bool _introVisible;
void Awake()
{
#if UNITY_SERVER
tutorialModeEnabled = false;
tutorialModeTrigger = false;
HideIntroImmediate(intro_shoot);
HideIntroImmediate(intro_goal);
enabled = false;
return;
#endif
tutorialModeEnabled = false;
if (isTutorial)
tutorialModeTrigger = true;
if (tutorialModeTrigger)
tutorialModeEnabled = true;
tutorialModeTrigger = false;
if (!tutorialModeEnabled)
{
HideIntroImmediate(intro_shoot);
HideIntroImmediate(intro_goal);
}
}
void OnEnable()
{
GameManager.OnTeamChanged += OnTeamChanged;
}
void OnDisable()
{
GameManager.OnTeamChanged -= OnTeamChanged;
}
void OnDestroy()
{
if (tutorialModeEnabled)
tutorialModeEnabled = false;
if (intro_shoot != null)
DOTween.Kill(intro_shoot);
if (intro_goal != null)
DOTween.Kill(intro_goal);
}
void Start()
{
if (tutorialModeEnabled)
{
NetManager.StopNetworkingForSceneChange();
NetworkManager.singleton.networkAddress = "localhost";
NetworkManager.singleton.StartHost();
WireIntroButtons();
HideIntroImmediate(intro_goal);
HideIntroImmediate(intro_shoot);
StartCoroutine(SetupAiWhenReady());
StartCoroutine(ShowShootIntroWhenReady());
}
}
void WireIntroButtons()
{
if (intro_shoot != null)
_introShootButton = intro_shoot.GetComponentInChildren<Button>(true);
if (intro_goal != null)
_introGoalButton = intro_goal.GetComponentInChildren<Button>(true);
if (_introShootButton != null)
_introShootButton.onClick.AddListener(OnShootIntroDismissed);
if (_introGoalButton != null)
_introGoalButton.onClick.AddListener(OnGoalIntroDismissed);
}
IEnumerator ShowShootIntroWhenReady()
{
while (GameManager.instance == null || !GameManager.instance.gameStarted)
yield return null;
while (NetPlayer.localPlayer == null)
yield return null;
ShowIntro(intro_shoot);
}
void OnTeamChanged(Team team)
{
if (!tutorialModeEnabled || _introVisible)
return;
if (team != GetPlayerTeam())
return;
if (!_shootIntroDismissed || _goalIntroDismissed)
return;
ShowIntro(intro_goal);
}
void OnShootIntroDismissed()
{
if (_shootIntroDismissed)
return;
_shootIntroDismissed = true;
DismissIntro(intro_shoot);
}
void OnGoalIntroDismissed()
{
if (_goalIntroDismissed)
return;
_goalIntroDismissed = true;
DismissIntro(intro_goal);
}
void ShowIntro(CanvasGroup group)
{
if (group == null)
return;
EnsureTutorialCanvasVisible(group);
DOTween.Kill(group);
group.gameObject.SetActive(true);
group.alpha = 1f;
group.interactable = true;
group.blocksRaycasts = true;
_introVisible = true;
if (GameManager.instance != null)
GameManager.instance.SetTurnTimerPaused(true);
}
void DismissIntro(CanvasGroup group)
{
if (group == null)
return;
group.interactable = false;
group.blocksRaycasts = false;
DOTween.Kill(group);
group.DOFade(0f, introFadeDuration)
.SetEase(Ease.InOutSine)
.OnComplete(() =>
{
group.gameObject.SetActive(false);
_introVisible = false;
if (GameManager.instance != null)
GameManager.instance.SetTurnTimerPaused(false);
});
}
static void HideIntroImmediate(CanvasGroup group)
{
if (group == null)
return;
DOTween.Kill(group);
group.alpha = 0f;
group.interactable = false;
group.blocksRaycasts = false;
group.gameObject.SetActive(false);
}
static void EnsureTutorialCanvasVisible(CanvasGroup group)
{
Transform root = group.transform.root;
if (root.localScale == Vector3.zero)
root.localScale = Vector3.one;
}
static Team GetPlayerTeam()
{
if (NetPlayer.localPlayer != null)
return NetPlayer.localPlayer.myTeam;
return GameManager.MyTeam;
}
/// <summary>Call from the main menu before loading the game scene.</summary>
public static void PrepareLaunch(MatchmadePlayer myPlayer, MatchmadePlayer cpuPlayer, Team myTeam = Team.Red)
{
LoginManager.ClearMatchYourTeam();
GameManager.MyTeam = myTeam;
GameManager.MatchRcPrizeCoins = 0;
GameManager.ConfigureDedicatedMatchReporting(0);
if (myTeam == Team.Red)
{
GameManager.RedPlayer = myPlayer;
GameManager.BluePlayer = cpuPlayer;
}
else
{
GameManager.RedPlayer = cpuPlayer;
GameManager.BluePlayer = myPlayer;
}
LevelLoadManager.myPlayer = myPlayer;
LevelLoadManager.opponentPlayer = cpuPlayer;
if (LevelLoadManager.instance != null)
LevelLoadManager.instance.SetupMatchMade(myTeam, myPlayer, cpuPlayer, "0 RC");
tutorialModeTrigger = true;
}
public static MatchmadePlayer CreateCpuOpponent()
{
GetCpuL10(out int wins, out int losses);
return new MatchmadePlayer
{
Name = CpuOpponentName,
l10_wins = wins,
l10_losses = losses
};
}
public static void GetCpuL10(out int wins, out int losses)
{
CountL10(PlayerPrefs.GetString(CpuL10PrefsKey, string.Empty), out wins, out losses);
}
public static void ApplyCpuL10(MatchmadePlayer cpu)
{
if (cpu == null)
return;
GetCpuL10(out int wins, out int losses);
cpu.l10_wins = wins;
cpu.l10_losses = losses;
}
/// <summary>
/// Records one practice result from the CPU's perspective (rolling last 10).
/// Cleared on logout because <see cref="LoginManager.Logout"/> calls PlayerPrefs.DeleteAll.
/// </summary>
public static void RecordCpuL10Result(bool cpuWon)
{
string history = PlayerPrefs.GetString(CpuL10PrefsKey, string.Empty) ?? string.Empty;
history += cpuWon ? 'W' : 'L';
if (history.Length > L10Window)
history = history.Substring(history.Length - L10Window);
PlayerPrefs.SetString(CpuL10PrefsKey, history);
PlayerPrefs.Save();
}
static void CountL10(string history, out int wins, out int losses)
{
wins = 0;
losses = 0;
if (string.IsNullOrEmpty(history))
return;
for (int i = 0; i < history.Length; i++)
{
if (history[i] == 'W')
wins++;
else if (history[i] == 'L')
losses++;
}
}
public static MatchmadePlayer CreateMyPlayerFromUser(UserData user)
{
return new MatchmadePlayer
{
Name = user.username,
UserId = user.id,
rc = user.rc
};
}
public static IEnumerator CoFetchPlayerL10(MatchmadePlayer player)
{
if (player == null || player.UserId <= 0)
yield break;
string baseUrl = (GameManager.DedicatedInternalApiBase ?? LoginManager.AuthBaseUrl ?? "").TrimEnd('/');
if (string.IsNullOrEmpty(baseUrl))
yield break;
string url = baseUrl + "/players/" + player.UserId + "/l10";
using (var req = UnityWebRequest.Get(url))
{
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
Logger.Log("Tutorial L10 fetch failed: " + req.error + " " + url);
yield break;
}
var resp = JsonUtility.FromJson<PlayerL10Response>(req.downloadHandler.text);
if (resp != null && resp.ok)
{
player.l10_wins = resp.l10_wins;
player.l10_losses = resp.l10_losses;
}
}
}
[Serializable]
class PlayerL10Response
{
public bool ok;
public int player_id;
public int l10_wins;
public int l10_losses;
}
IEnumerator SetupAiWhenReady()
{
float timeout = 5f;
while (NetPlayer.localPlayer == null && timeout > 0f)
{
timeout -= Time.deltaTime;
yield return null;
}
Team playerTeam = NetPlayer.localPlayer != null
? NetPlayer.localPlayer.myTeam
: GameManager.MyTeam;
Team opponentTeam = playerTeam == Team.Red ? Team.Blue : Team.Red;
if (GameManager.instance.SelectedTeam != playerTeam)
GameManager.instance.SwitchTeams();
aiController.Setup(opponentTeam);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 304204825fc486545a7a9bd7dd232861
+19
View File
@@ -4,9 +4,24 @@ using UnityEngine;
public class Goal : NetworkBehaviour public class Goal : NetworkBehaviour
{ {
public Team team; public Team team;
public static Goal RedGoal,BlueGoal;
void Awake()
{
if(team == Team.Red){
RedGoal = this;
}else{
BlueGoal = this;
}
}
void OnTriggerEnter2D(Collider2D collision) void OnTriggerEnter2D(Collider2D collision)
{ {
if(!isServer){return;} if(!isServer){return;}
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
return;
if(collision.gameObject.CompareTag("Player")){ if(collision.gameObject.CompareTag("Player")){
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal){ if(GameManager.instance == null || !GameManager.instance.CanProcessGoal){
@@ -17,6 +32,8 @@ public class Goal : NetworkBehaviour
Debug.Log(ball.name); Debug.Log(ball.name);
GameManager.instance.OnGoal(team); GameManager.instance.OnGoal(team);
}else if(collision.gameObject.CompareTag("Puck")){ }else if(collision.gameObject.CompareTag("Puck")){
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal)
return;
Debug.Log("Puck goal", gameObject); Debug.Log("Puck goal", gameObject);
Puck puck = collision.gameObject.GetComponent<Puck>(); Puck puck = collision.gameObject.GetComponent<Puck>();
Debug.Log(puck.name); Debug.Log(puck.name);
@@ -29,6 +46,8 @@ public class Goal : NetworkBehaviour
if(!isServer){return;} if(!isServer){return;}
if(collision.gameObject.CompareTag("Puck")){ if(collision.gameObject.CompareTag("Puck")){
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
return;
Debug.Log("Puck exit goal", gameObject); Debug.Log("Puck exit goal", gameObject);
Puck puck = collision.gameObject.GetComponent<Puck>(); Puck puck = collision.gameObject.GetComponent<Puck>();
Debug.Log(puck.name); Debug.Log(puck.name);
+506 -28
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
#if UNITY_EDITOR #if UNITY_EDITOR
@@ -13,13 +14,14 @@ using UnityEngine.UI;
public class LoginManager : MonoBehaviour public class LoginManager : MonoBehaviour
{ {
public const string AuthBaseUrl = "https://kickkingsapi.playpoolstudios.com"; public const string AuthBaseUrl = "https://kkapi.playpoolstudios.com";
const string PlayerPrefsAuthKey = "WalkInvest.AuthToken"; const string PlayerPrefsAuthKey = "WalkInvest.AuthToken";
const int MIN_USERNAME_LENGTH = 3; const int MIN_USERNAME_LENGTH = 3;
const int MAX_USERNAME_LENGTH = 16; const int MAX_USERNAME_LENGTH = 16;
const int MIN_PASSWORD_LENGTH = 8; const int MIN_PASSWORD_LENGTH = 8;
const int MAX_PASSWORD_LENGTH = 20; const int MAX_PASSWORD_LENGTH = 20;
const int ShortAuthMessageMaxLength = 36;
[Header("Login")] [Header("Login")]
[SerializeField] private TMP_InputField usernameInputLogin; [SerializeField] private TMP_InputField usernameInputLogin;
@@ -27,13 +29,36 @@ public class LoginManager : MonoBehaviour
[SerializeField] private Button btnLogin; [SerializeField] private Button btnLogin;
[Header("Register")] [Header("Register")]
[SerializeField] private TMP_InputField usernameInputRegister; [SerializeField] private TMP_InputField usernameInputRegister;
[SerializeField] private TMP_InputField emailInputRegister;
[SerializeField] private TMP_InputField passwordInputRegister; [SerializeField] private TMP_InputField passwordInputRegister;
[SerializeField] private Button btnRegister; [SerializeField] private Button btnRegister;
[Header("UI")] [Header("Server messages (GET /table_settings, no auth)")]
[SerializeField] private TMP_Text error_txt; [Header("Update")]
[SerializeField] private GameObject serverUpdatePanel;
[SerializeField] private Button btnServerUpdate;
[Tooltip("Opened when the user taps Update on the forced-update panel (e.g. Telegram channel).")]
public string telegramUpdateChannelUrl = "";
[Header("Error")]
[SerializeField] private GameObject errorPanel;
[SerializeField] private TMP_Text serverErrorMessageTxt;
[SerializeField] private Button btnErrorClose;
[Header("Warning")]
[SerializeField] private GameObject warningPanel;
[SerializeField] private TMP_Text serverWarningMessageTxt;
[SerializeField] private Button btnWarningClose;
const float KeepaliveIntervalSeconds = 10f;
bool _authInProgress; bool _authInProgress;
Coroutine _keepaliveRoutine;
bool _appPaused;
bool _appFocused = true;
bool _handlingKeepaliveUnauthorized;
string _pendingAuthMessage;
public static string AuthToken { get; private set; } public static string AuthToken { get; private set; }
@@ -84,27 +109,59 @@ public class LoginManager : MonoBehaviour
void Awake() void Awake()
{ {
error_txt.text = ""; HideServerMessagePanels();
AuthToken = PlayerPrefs.GetString(PlayerPrefsAuthKey, "");
if(instance !=null){ if (instance != null && instance != this)
Destroy(gameObject); {
return; instance.StopKeepalive();
SceneManager.sceneLoaded -= instance.OnSceneLoaded;
Destroy(instance.gameObject);
} }
instance = this; instance = this;
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
SceneManager.sceneLoaded += OnSceneLoaded;
AuthToken = PlayerPrefs.GetString(PlayerPrefsAuthKey, "");
if (string.IsNullOrEmpty(AuthToken)) if (string.IsNullOrEmpty(AuthToken))
ClearLocalUserProfile(); ClearLocalUserProfile();
#if !UNITY_EDITOR
else
StartKeepalive();
#endif
} }
void Start() void Start()
{ {
btnLogin.onClick.AddListener(OnLogin); btnLogin.onClick.AddListener(OnLogin);
btnRegister.onClick.AddListener(OnRegister); btnRegister.onClick.AddListener(OnRegister);
if (btnServerUpdate != null)
btnServerUpdate.onClick.AddListener(OnServerUpdateClick);
if (btnErrorClose != null)
btnErrorClose.onClick.AddListener(OnServerErrorCloseClick);
SetBusy(true);
StartCoroutine(StartupSequenceCoroutine());
}
IEnumerator StartupSequenceCoroutine()
{
bool mayContinue = false;
yield return StartCoroutine(CoApplyTableSettingsGate(r => mayContinue = r));
if (!mayContinue)
{
// Blocked by update or error panel — keep login UI disabled, but hold the session.
if (!string.IsNullOrEmpty(AuthToken))
StartKeepalive();
SetBusy(false);
yield break;
}
#if UNITY_EDITOR #if UNITY_EDITOR
// Dev: log in with clone-aware credentials instead of resuming a saved session. // Dev: log in with clone-aware credentials instead of resuming a saved session.
StopKeepalive();
AuthToken = ""; AuthToken = "";
PlayerPrefs.DeleteKey(PlayerPrefsAuthKey); PlayerPrefs.DeleteKey(PlayerPrefsAuthKey);
PlayerPrefs.Save(); PlayerPrefs.Save();
@@ -117,12 +174,15 @@ public class LoginManager : MonoBehaviour
string loginJson = "{\"username\":\"" + EscapeJsonString(devUser) + "\",\"password\":\"" + EscapeJsonString(devPass) + "\"}"; string loginJson = "{\"username\":\"" + EscapeJsonString(devUser) + "\",\"password\":\"" + EscapeJsonString(devPass) + "\"}";
// StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/login", loginJson, "Signed in.")); // StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/login", loginJson, "Signed in."));
SetBusy(false);
#else #else
if (!string.IsNullOrEmpty(AuthToken)) if (!string.IsNullOrEmpty(AuthToken))
{ {
SetBusy(true); StartKeepalive();
StartCoroutine(ResumeSessionCoroutine()); yield return StartCoroutine(ResumeSessionCoroutine());
} }
else
SetBusy(false);
#endif #endif
} }
@@ -160,8 +220,7 @@ public class LoginManager : MonoBehaviour
else else
{ {
InvalidateStoredSession(); InvalidateStoredSession();
if (error_txt != null) ShowAuthMessage(string.IsNullOrEmpty(errMsg) ? "Session expired. Please sign in again." : errMsg);
error_txt.text = string.IsNullOrEmpty(errMsg) ? "Session expired. Please sign in again." : errMsg;
} }
SetBusy(false); SetBusy(false);
@@ -176,6 +235,8 @@ public class LoginManager : MonoBehaviour
/// <summary>Removes auth token from memory and disk and clears cached profile (login screen recovery).</summary> /// <summary>Removes auth token from memory and disk and clears cached profile (login screen recovery).</summary>
static void InvalidateStoredSession() static void InvalidateStoredSession()
{ {
if (instance != null)
instance.StopKeepalive();
AuthToken = ""; AuthToken = "";
ClearLocalUserProfile(); ClearLocalUserProfile();
ClearMatchYourTeam(); ClearMatchYourTeam();
@@ -183,6 +244,31 @@ public class LoginManager : MonoBehaviour
PlayerPrefs.Save(); PlayerPrefs.Save();
} }
/// <summary>Clears session state and all PlayerPrefs (full logout).</summary>
public static void Logout()
{
if (instance != null)
instance.StopKeepalive();
AuthToken = "";
ClearLocalUserProfile();
ClearMatchYourTeam();
OnUserDataUpdated = null;
Cupid.ClearSession();
CupidLobby.ClearUsername();
GameManager.ClearMatchSession();
NetPlayer.RedPlayer = null;
NetPlayer.BluePlayer = null;
PlayerPrefs.DeleteAll();
PlayerPrefs.Save();
}
/// <summary>Full logout, then reload the login scene so a fresh <see cref="LoginManager"/> can bind UI.</summary>
public static void LogoutAndReturnToLogin()
{
Logout();
SceneManager.LoadScene(0);
}
static void ProceedToMainMenu() static void ProceedToMainMenu()
{ {
if (LevelLoadManager.instance != null) if (LevelLoadManager.instance != null)
@@ -198,27 +284,62 @@ public class LoginManager : MonoBehaviour
if (btnRegister != null) btnRegister.interactable = !busy; if (btnRegister != null) btnRegister.interactable = !busy;
} }
void ShowAuthMessage(string message)
{
if (string.IsNullOrEmpty(message))
return;
if (MessageBoxDialog.instance == null)
{
_pendingAuthMessage = message;
return;
}
_pendingAuthMessage = null;
if (message.Length <= ShortAuthMessageMaxLength)
MessageBoxDialog.instance.ShowMessageBox(message);
else
MessageBoxDialog.instance.ShowMessageBox("Error", message);
}
void HideAuthMessage()
{
if (MessageBoxDialog.instance != null)
MessageBoxDialog.instance.HideMessageBox();
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (string.IsNullOrEmpty(_pendingAuthMessage) || MessageBoxDialog.instance == null)
return;
string message = _pendingAuthMessage;
_pendingAuthMessage = null;
ShowAuthMessage(message);
}
void OnLogin() void OnLogin()
{ {
if (_authInProgress) return; if (_authInProgress) return;
if (_keepaliveRoutine != null && !string.IsNullOrEmpty(AuthToken)) return;
string username = usernameInputLogin.text; string username = usernameInputLogin.text;
string password = passwordInputLogin.text; string password = passwordInputLogin.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{ {
error_txt.text = "Please fill in all fields"; ShowAuthMessage("Please fill in all fields");
return; return;
} }
if (username.Length < MIN_USERNAME_LENGTH || username.Length > MAX_USERNAME_LENGTH) if (username.Length < MIN_USERNAME_LENGTH || username.Length > MAX_USERNAME_LENGTH)
{ {
error_txt.text = "Username must be between " + MIN_USERNAME_LENGTH + " and " + MAX_USERNAME_LENGTH + " characters"; ShowAuthMessage("Username must be between " + MIN_USERNAME_LENGTH + " and " + MAX_USERNAME_LENGTH + " characters");
return; return;
} }
if (password.Length < MIN_PASSWORD_LENGTH || password.Length > MAX_PASSWORD_LENGTH) if (password.Length < MIN_PASSWORD_LENGTH || password.Length > MAX_PASSWORD_LENGTH)
{ {
error_txt.text = "Password must be between " + MIN_PASSWORD_LENGTH + " and " + MAX_PASSWORD_LENGTH + " characters"; ShowAuthMessage("Password must be between " + MIN_PASSWORD_LENGTH + " and " + MAX_PASSWORD_LENGTH + " characters");
return; return;
} }
@@ -229,32 +350,66 @@ public class LoginManager : MonoBehaviour
void OnRegister() void OnRegister()
{ {
if (_authInProgress) return; if (_authInProgress) return;
if (_keepaliveRoutine != null && !string.IsNullOrEmpty(AuthToken)) return;
string username = usernameInputRegister.text; string username = usernameInputRegister.text;
string email = emailInputRegister != null ? emailInputRegister.text : "";
string password = passwordInputRegister.text; string password = passwordInputRegister.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
{ {
error_txt.text = "Please fill in all fields"; ShowAuthMessage("Please fill in all fields");
return;
}
email = email.Trim().ToLowerInvariant();
if (!IsValidEmailFormat(email))
{
ShowAuthMessage("Please enter a valid email address");
return; return;
} }
if (username.Length < MIN_USERNAME_LENGTH || username.Length > MAX_USERNAME_LENGTH) if (username.Length < MIN_USERNAME_LENGTH || username.Length > MAX_USERNAME_LENGTH)
{ {
error_txt.text = "Username must be between " + MIN_USERNAME_LENGTH + " and " + MAX_USERNAME_LENGTH + " characters"; ShowAuthMessage("Username must be between " + MIN_USERNAME_LENGTH + " and " + MAX_USERNAME_LENGTH + " characters");
return; return;
} }
if (password.Length < MIN_PASSWORD_LENGTH || password.Length > MAX_PASSWORD_LENGTH) if (password.Length < MIN_PASSWORD_LENGTH || password.Length > MAX_PASSWORD_LENGTH)
{ {
error_txt.text = "Password must be between " + MIN_PASSWORD_LENGTH + " and " + MAX_PASSWORD_LENGTH + " characters"; ShowAuthMessage("Password must be between " + MIN_PASSWORD_LENGTH + " and " + MAX_PASSWORD_LENGTH + " characters");
return; return;
} }
// Server register endpoint accepts username + password only (email field is optional UI-only). string json = "{\"username\":\"" + EscapeJsonString(username) + "\",\"email\":\"" + EscapeJsonString(email) + "\",\"password\":\"" + EscapeJsonString(password) + "\"}";
string json = "{\"username\":\"" + EscapeJsonString(username) + "\",\"password\":\"" + EscapeJsonString(password) + "\"}";
StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/register", json, "Account created.")); StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/register", json, "Account created."));
} }
static bool IsValidEmailFormat(string email)
{
if (string.IsNullOrEmpty(email))
return false;
int at = email.IndexOf('@');
if (at <= 0)
return false;
if (email.IndexOf('@', at + 1) >= 0)
return false;
string domain = email.Substring(at + 1);
int dot = domain.LastIndexOf('.');
if (dot <= 0 || dot >= domain.Length - 1)
return false;
for (int i = 0; i < email.Length; i++)
{
if (char.IsWhiteSpace(email[i]))
return false;
}
return true;
}
static string EscapeJsonString(string s) static string EscapeJsonString(string s)
{ {
if (string.IsNullOrEmpty(s)) return s; if (string.IsNullOrEmpty(s)) return s;
@@ -264,7 +419,7 @@ public class LoginManager : MonoBehaviour
IEnumerator AuthPostCoroutine(string url, string jsonBody, string successMessage) IEnumerator AuthPostCoroutine(string url, string jsonBody, string successMessage)
{ {
SetBusy(true); SetBusy(true);
error_txt.text = ""; HideAuthMessage();
ClearLocalUserProfile(); ClearLocalUserProfile();
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST)) using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
@@ -281,14 +436,14 @@ public class LoginManager : MonoBehaviour
if (request.result != UnityWebRequest.Result.Success && if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError) request.result != UnityWebRequest.Result.ProtocolError)
{ {
error_txt.text = string.IsNullOrEmpty(request.error) ? "Network error" : request.error; ShowAuthMessage("Network Error, Please try again.");
SetBusy(false); SetBusy(false);
yield break; yield break;
} }
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
{ {
error_txt.text = "Empty response from server"; ShowAuthMessage("Empty response from server");
SetBusy(false); SetBusy(false);
yield break; yield break;
} }
@@ -300,6 +455,7 @@ public class LoginManager : MonoBehaviour
AuthToken = resp.token; AuthToken = resp.token;
PlayerPrefs.SetString(PlayerPrefsAuthKey, resp.token); PlayerPrefs.SetString(PlayerPrefsAuthKey, resp.token);
PlayerPrefs.Save(); PlayerPrefs.Save();
StartKeepalive();
bool profileOk = false; bool profileOk = false;
string profileErr = null; string profileErr = null;
@@ -311,20 +467,163 @@ public class LoginManager : MonoBehaviour
if (profileOk) if (profileOk)
ProceedToMainMenu(); ProceedToMainMenu();
else if (error_txt != null) else
error_txt.text = string.IsNullOrEmpty(profileErr) ShowAuthMessage(string.IsNullOrEmpty(profileErr)
? "Signed in but could not load your profile. Check your connection and try again." ? "Signed in but could not load your profile. Check your connection and try again."
: profileErr; : profileErr);
} }
else else
{ {
error_txt.text = string.IsNullOrEmpty(resp.error) ? "Request failed" : resp.error; ShowAuthMessage("Login failed. Please try again.");
} }
} }
SetBusy(false); SetBusy(false);
} }
void HideServerMessagePanels()
{
if (serverUpdatePanel != null) serverUpdatePanel.SetActive(false);
if (errorPanel != null) errorPanel.SetActive(false);
if (warningPanel != null) warningPanel.SetActive(false);
}
void OnServerUpdateClick()
{
if (!string.IsNullOrWhiteSpace(telegramUpdateChannelUrl))
Application.OpenURL(telegramUpdateChannelUrl.Trim());
}
void OnServerErrorCloseClick()
{
#if UNITY_EDITOR
EditorApplication.ExitPlaymode();
#else
Application.Quit();
#endif
}
/// <summary>
/// GET <c>/table_settings</c> (public) and applies version / error / warning gates.
/// Callback receives <c>true</c> when login may continue to the main menu.
/// </summary>
IEnumerator CoApplyTableSettingsGate(Action<bool> onComplete)
{
CupidTableSettings table = null;
yield return StartCoroutine(FetchTableSettingsCoroutine(t => table = t));
if (table == null)
{
onComplete?.Invoke(true);
yield break;
}
if (IsClientBelowRequiredVersion(table.version))
{
if (serverUpdatePanel != null)
serverUpdatePanel.SetActive(true);
onComplete?.Invoke(false);
yield break;
}
if (!string.IsNullOrWhiteSpace(table.error_msg))
{
if (serverErrorMessageTxt != null)
serverErrorMessageTxt.text = table.error_msg.Trim();
if (errorPanel != null)
errorPanel.SetActive(true);
onComplete?.Invoke(false);
yield break;
}
if (!string.IsNullOrWhiteSpace(table.warning_msg))
yield return CoWaitForWarningDismiss(table.warning_msg.Trim());
onComplete?.Invoke(true);
}
IEnumerator FetchTableSettingsCoroutine(Action<CupidTableSettings> onResult)
{
string url = AuthBaseUrl + "/table_settings";
using (var request = UnityWebRequest.Get(url))
{
request.downloadHandler = new DownloadHandlerBuffer();
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
onResult?.Invoke(null);
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300 || string.IsNullOrWhiteSpace(text))
{
onResult?.Invoke(null);
yield break;
}
try
{
CupidTableSettings parsed = JsonUtility.FromJson<CupidTableSettings>(text);
onResult?.Invoke(parsed);
}
catch (Exception e)
{
Logger.Log("table_settings JSON error: " + e.Message + " body=" + text);
onResult?.Invoke(null);
}
}
}
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;
}
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);
}
IEnumerator CoWaitForWarningDismiss(string message)
{
if (serverWarningMessageTxt != null)
serverWarningMessageTxt.text = message;
if (warningPanel != null)
warningPanel.SetActive(true);
bool dismissed = false;
void OnDismiss()
{
dismissed = true;
if (warningPanel != null)
warningPanel.SetActive(false);
}
if (btnWarningClose != null)
{
btnWarningClose.onClick.RemoveListener(OnDismiss);
btnWarningClose.onClick.AddListener(OnDismiss);
}
yield return new WaitUntil(() => dismissed);
if (btnWarningClose != null)
btnWarningClose.onClick.RemoveListener(OnDismiss);
}
static void ApplyUserData(UserData u) static void ApplyUserData(UserData u)
{ {
_currentUser = u; _currentUser = u;
@@ -397,6 +696,12 @@ public class LoginManager : MonoBehaviour
StartCoroutine(PurchaseRcCoroutine(packId, onComplete)); StartCoroutine(PurchaseRcCoroutine(packId, onComplete));
} }
/// <summary>POST /auth/ping-report with <c>{"match_id":123,"ping":42}</c>. Callback (success, errorOrNull, reportOrNull).</summary>
public void ReportPing(int matchId, int ping, Action<bool, string, PingReportData> onComplete)
{
StartCoroutine(ReportPingCoroutine(matchId, ping, onComplete));
}
IEnumerator PurchaseRcCoroutine(string packId, Action<bool, string> onComplete) IEnumerator PurchaseRcCoroutine(string packId, Action<bool, string> onComplete)
{ {
if (string.IsNullOrEmpty(AuthToken)) if (string.IsNullOrEmpty(AuthToken))
@@ -470,6 +775,59 @@ public class LoginManager : MonoBehaviour
} }
} }
IEnumerator ReportPingCoroutine(int matchId, int ping, Action<bool, string, PingReportData> onComplete)
{
if (string.IsNullOrEmpty(AuthToken))
{
onComplete?.Invoke(false, "Not signed in", null);
yield break;
}
string json = "{\"match_id\":" + matchId + ",\"ping\":" + ping + "}";
string url = AuthBaseUrl + "/auth/ping-report";
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", "Bearer " + AuthToken);
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
yield break;
}
if (string.IsNullOrEmpty(text))
{
onComplete?.Invoke(false, "Empty response from server", null);
yield break;
}
PingReportApiResponse resp = JsonUtility.FromJson<PingReportApiResponse>(text);
if (resp != null && resp.ok && resp.report != null)
onComplete?.Invoke(true, null, resp.report);
else
{
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Ping report failed";
onComplete?.Invoke(false, err, null);
}
}
}
IEnumerator FetchUserDataCoroutine(Action<bool, string, UserData> onComplete, bool applyToStatic) IEnumerator FetchUserDataCoroutine(Action<bool, string, UserData> onComplete, bool applyToStatic)
{ {
if (string.IsNullOrEmpty(AuthToken)) if (string.IsNullOrEmpty(AuthToken))
@@ -522,4 +880,124 @@ public class LoginManager : MonoBehaviour
} }
} }
} }
bool IsAppInForeground => !_appPaused && _appFocused;
void OnApplicationPause(bool paused)
{
_appPaused = paused;
}
void OnApplicationFocus(bool hasFocus)
{
_appFocused = hasFocus;
}
void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
StopKeepalive();
if (instance == this)
instance = null;
}
void StartKeepalive()
{
if (string.IsNullOrEmpty(AuthToken)) return;
if (_keepaliveRoutine != null) return;
_handlingKeepaliveUnauthorized = false;
_keepaliveRoutine = StartCoroutine(KeepaliveLoopCoroutine());
}
void StopKeepalive()
{
if (_keepaliveRoutine == null) return;
StopCoroutine(_keepaliveRoutine);
_keepaliveRoutine = null;
}
IEnumerator KeepaliveLoopCoroutine()
{
while (!string.IsNullOrEmpty(AuthToken))
{
while (!IsAppInForeground && !string.IsNullOrEmpty(AuthToken))
yield return null;
if (string.IsNullOrEmpty(AuthToken))
break;
yield return KeepalivePingCoroutine();
if (string.IsNullOrEmpty(AuthToken))
break;
float elapsed = 0f;
while (elapsed < KeepaliveIntervalSeconds && !string.IsNullOrEmpty(AuthToken))
{
if (!IsAppInForeground)
{
while (!IsAppInForeground && !string.IsNullOrEmpty(AuthToken))
yield return null;
break;
}
elapsed += Time.unscaledDeltaTime;
yield return null;
}
}
_keepaliveRoutine = null;
}
IEnumerator KeepalivePingCoroutine()
{
if (string.IsNullOrEmpty(AuthToken))
yield break;
string url = AuthBaseUrl + "/auth/keepalive";
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
request.uploadHandler = new UploadHandlerRaw(Array.Empty<byte>());
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", "Bearer " + AuthToken);
yield return request.SendWebRequest();
if (request.responseCode == 401)
{
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
string err = "Session expired. Please sign in again.";
if (!string.IsNullOrEmpty(text))
{
AuthApiResponse resp = JsonUtility.FromJson<AuthApiResponse>(text);
if (resp != null && !string.IsNullOrEmpty(resp.error))
err = resp.error;
}
HandleKeepaliveUnauthorized(err);
yield break;
}
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
Logger.Log("auth/keepalive network error: " + (string.IsNullOrEmpty(request.error) ? "Network error" : request.error));
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300)
Logger.Log("auth/keepalive failed: HTTP " + request.responseCode);
}
}
void HandleKeepaliveUnauthorized(string message)
{
if (_handlingKeepaliveUnauthorized) return;
_handlingKeepaliveUnauthorized = true;
SetBusy(false);
ShowAuthMessage(string.IsNullOrEmpty(message) ? "Session expired. Please sign in again." : message);
LogoutAndReturnToLogin();
}
} }
+187 -28
View File
@@ -25,11 +25,15 @@ public class MainMenuManager : MonoBehaviour
public GameObject gameModesScreen; public GameObject gameModesScreen;
public GameObject dummyBuyScreen; public GameObject dummyBuyScreen;
public GameObject withdrawalScreen; public GameObject withdrawalScreen;
public GameObject settingsScreen;
[Header("Play")] [Header("Play")]
public Button btnPlay; public Button btnPlay;
public Button btnTutorial;
public TMP_Text rcPlayText; public TMP_Text rcPlayText;
public TMP_Text entryFeeTxt; public TMP_Text entryFeeTxt;
public TMP_Text rcPrizeTxt; public TMP_Text rcPrizeTxt;
[SerializeField] string tutorialGameScene = GameTutorialManager.DefaultGameSceneName;
[Header("Dummy Buy")] [Header("Dummy Buy")]
public Button btnBuyUsd5; public Button btnBuyUsd5;
@@ -43,6 +47,14 @@ public class MainMenuManager : MonoBehaviour
public TMP_Text txtWithdrawStatus; public TMP_Text txtWithdrawStatus;
private bool isCoinBuyInProgress; private bool isCoinBuyInProgress;
[Header("Settings")]
public ProfileInfoFetcher profileInfoFetcher;
public TMP_Text txtUsername;
public TMP_Text txtEmail;
public TMP_Text txtAge,txtMatches,txtWinRatio;
public TMP_Text txtId;
public Button btnLogout;
[Header("Audio")] [Header("Audio")]
public ToggleButton toggleSfx; public ToggleButton toggleSfx;
public ToggleButton toggleMusic; public ToggleButton toggleMusic;
@@ -73,6 +85,7 @@ public class MainMenuManager : MonoBehaviour
if (success) if (success)
{ {
OnUserUpdated(user); OnUserUpdated(user);
RefreshSettingsProfile();
} }
else else
{ {
@@ -83,6 +96,8 @@ public class MainMenuManager : MonoBehaviour
} }
}); });
RefreshSettingsProfile();
btnBuyUsd5.onClick.AddListener(() => OnBuy(RcPack.Usd5)); btnBuyUsd5.onClick.AddListener(() => OnBuy(RcPack.Usd5));
btnBuyUsd20.onClick.AddListener(() => OnBuy(RcPack.Usd20)); btnBuyUsd20.onClick.AddListener(() => OnBuy(RcPack.Usd20));
btnBuyUsd50.onClick.AddListener(() => OnBuy(RcPack.Usd50)); btnBuyUsd50.onClick.AddListener(() => OnBuy(RcPack.Usd50));
@@ -93,12 +108,18 @@ public class MainMenuManager : MonoBehaviour
inputCryptoAddress.onValueChanged.AddListener(OnCryptoAddressChanged); inputCryptoAddress.onValueChanged.AddListener(OnCryptoAddressChanged);
} }
UpdateWithdrawalStatus(); if (txtWithdrawStatus != null)
txtWithdrawStatus.gameObject.SetActive(false);
if (btnWithdrawal != null)
btnWithdrawal.onClick.AddListener(OnWithdrawalPressed);
if (btnPaste != null) if (btnPaste != null)
btnPaste.onClick.AddListener(OnPasteFromClipboard); btnPaste.onClick.AddListener(OnPasteFromClipboard);
btnPlay.onClick.AddListener(OnPlay); btnPlay.onClick.AddListener(OnPlay);
if (btnTutorial != null && btnTutorial != btnPlay)
btnTutorial.onClick.AddListener(OnPlayTutorial);
toggleSfx.onToggleValueChanged.AddListener(OnToggleSfx); toggleSfx.onToggleValueChanged.AddListener(OnToggleSfx);
toggleMusic.onToggleValueChanged.AddListener(OnToggleMusic); toggleMusic.onToggleValueChanged.AddListener(OnToggleMusic);
@@ -106,6 +127,9 @@ public class MainMenuManager : MonoBehaviour
toggleSfx.SetIsOn(AudioManager.instance.IsSFXOn()); toggleSfx.SetIsOn(AudioManager.instance.IsSFXOn());
toggleMusic.SetIsOn(AudioManager.instance.IsMusicOn()); toggleMusic.SetIsOn(AudioManager.instance.IsMusicOn());
if (btnLogout != null)
btnLogout.onClick.AddListener(OnLogout);
if (!LevelLoadManager.BlocksMainMenuSettingsBootstrap) if (!LevelLoadManager.BlocksMainMenuSettingsBootstrap)
StartCoroutine(CoBootstrapSettingsWithoutLoader()); StartCoroutine(CoBootstrapSettingsWithoutLoader());
} }
@@ -114,6 +138,35 @@ public class MainMenuManager : MonoBehaviour
LoginManager.OnUserDataUpdated -= OnUserUpdated; LoginManager.OnUserDataUpdated -= OnUserUpdated;
toggleSfx.onToggleValueChanged.RemoveListener(OnToggleSfx); toggleSfx.onToggleValueChanged.RemoveListener(OnToggleSfx);
toggleMusic.onToggleValueChanged.RemoveListener(OnToggleMusic); toggleMusic.onToggleValueChanged.RemoveListener(OnToggleMusic);
if (btnTutorial != null && btnTutorial != btnPlay)
btnTutorial.onClick.RemoveListener(OnPlayTutorial);
if (btnLogout != null)
btnLogout.onClick.RemoveListener(OnLogout);
if (btnWithdrawal != null)
btnWithdrawal.onClick.RemoveListener(OnWithdrawalPressed);
}
void OnLogout()
{
if (MessageBoxDialog.IsAvailable)
{
MessageBoxDialog.Show(
"Log out?",
"You will need to sign in again to play.",
confirmed =>
{
if (confirmed)
ConfirmLogout();
});
return;
}
ConfirmLogout();
}
void ConfirmLogout()
{
LoginManager.LogoutAndReturnToLogin();
} }
void OnToggleSfx(bool isOn) void OnToggleSfx(bool isOn)
@@ -134,8 +187,18 @@ public class MainMenuManager : MonoBehaviour
return; return;
} }
if (LoginManager.CurrentUser.rc < _matchEntryFeeCoins) if (LoginManager.CurrentUser.rc < _matchEntryFeeCoins)
{
string fee = CoinHelper.FormatString(_matchEntryFeeCoins) + " RC";
if (MessageBoxDialog.IsAvailable)
{
MessageBoxDialog.Show(
"Not enough RC",
"You need " + fee + " to play. Buy more RC and try again.");
}
else
{ {
Debug.LogError("Not enough RC to play"); Debug.LogError("Not enough RC to play");
}
return; return;
} }
if (CupidLobby.instance == null) if (CupidLobby.instance == null)
@@ -146,37 +209,82 @@ public class MainMenuManager : MonoBehaviour
CupidLobby.instance.Matchmake(); CupidLobby.instance.Matchmake();
} }
public void OnPlayTutorial()
{
if (LoginManager.CurrentUser == null)
{
Debug.LogError("Tutorial blocked: user profile not loaded.");
return;
}
if (LevelLoadManager.instance == null)
{
Debug.LogError("Tutorial blocked: LevelLoadManager is missing.");
return;
}
StartCoroutine(CoLaunchTutorial());
}
IEnumerator CoLaunchTutorial()
{
MatchmadePlayer myPlayer = GameTutorialManager.CreateMyPlayerFromUser(LoginManager.CurrentUser);
MatchmadePlayer cpuPlayer = GameTutorialManager.CreateCpuOpponent();
yield return GameTutorialManager.CoFetchPlayerL10(myPlayer);
GameTutorialManager.PrepareLaunch(myPlayer, cpuPlayer);
LevelLoadManager.LoadLevel(tutorialGameScene);
}
void OnCryptoAddressChanged(string value) void OnCryptoAddressChanged(string value)
{ {
PlayerPrefs.SetString(CryptoAddressPrefsKey, value ?? string.Empty); PlayerPrefs.SetString(CryptoAddressPrefsKey, value ?? string.Empty);
PlayerPrefs.Save(); PlayerPrefs.Save();
UpdateWithdrawalStatus();
} }
void UpdateWithdrawalStatus(UserData user = null) void OnWithdrawalPressed()
{ {
if (txtWithdrawStatus == null) UserData user = LoginManager.CurrentUser;
return; if (user == null)
UserData u = user ?? LoginManager.CurrentUser; {
if (u == null) ShowWithdrawalMessage("Could not load your profile. Try again.", false);
return; return;
}
if (inputCryptoAddress != null && string.IsNullOrWhiteSpace(inputCryptoAddress.text)) if (inputCryptoAddress != null && string.IsNullOrWhiteSpace(inputCryptoAddress.text))
{ {
txtWithdrawStatus.text = "Enter a valid wallet address to withdraw safely."; ShowWithdrawalMessage("Enter a valid wallet address to withdraw safely.", true);
return; return;
} }
if (u.rc < MinimumWithdrawalRcCoins)
if (user.rc < MinimumWithdrawalRcCoins)
{ {
txtWithdrawStatus.text = "Your RC balance is below the minimum withdrawal amount ($20 USD / 20.0 RC)."; ShowWithdrawalMessage("Your RC balance is below the minimum withdrawal amount ($20 USD / 20.0 RC).", false);
return; return;
} }
if (u.cc < WithdrawalFeeCc)
if (user.cc < WithdrawalFeeCc)
{ {
txtWithdrawStatus.text = "Withdrawal costs 500 CC. Your CC balance is below 500 CC."; ShowWithdrawalMessage("Withdrawal costs 500 CC. Your CC balance is below 500 CC.", false);
return; return;
} }
txtWithdrawStatus.text = string.Empty; }
void ShowWithdrawalMessage(string message, bool basic)
{
if (MessageBoxDialog.IsAvailable)
{
if (basic)
MessageBoxDialog.ShowBasic(message);
else
MessageBoxDialog.Show("Can't withdraw", message);
return;
}
if (txtWithdrawStatus != null)
{
txtWithdrawStatus.gameObject.SetActive(true);
txtWithdrawStatus.text = message;
}
} }
void OnPasteFromClipboard() void OnPasteFromClipboard()
@@ -229,20 +337,12 @@ public class MainMenuManager : MonoBehaviour
ccText.text = user.cc.ToString("N0") + " CC"; ccText.text = user.cc.ToString("N0") + " CC";
rcText.text = CoinHelper.FormatString(user.rc) + " RC"; rcText.text = CoinHelper.FormatString(user.rc) + " RC";
if (LoginManager.CurrentUser.rc < _matchEntryFeeCoins) if (rcPlayText != null)
{
rcPlayText.text = "Not enough RC to play";
btnPlay.interactable = false;
}
else
{
rcPlayText.text = ""; rcPlayText.text = "";
if (btnPlay != null)
btnPlay.interactable = true; btnPlay.interactable = true;
} }
UpdateWithdrawalStatus(user);
}
/// <summary>Called by <see cref="LevelLoadManager"/> so the loading overlay stays until matchmaker settings are refreshed.</summary> /// <summary>Called by <see cref="LevelLoadManager"/> so the loading overlay stays until matchmaker settings are refreshed.</summary>
public IEnumerator FinishLoadingScreenAndApplySettings() public IEnumerator FinishLoadingScreenAndApplySettings()
{ {
@@ -300,37 +400,96 @@ public class MainMenuManager : MonoBehaviour
} }
void SetStatsPanelVisible(bool visible)
{
if (statsPanel == null)
return;
statsPanel.SetActive(visible);
Transform topPanel = statsPanel.transform.parent;
if (topPanel != null)
topPanel.gameObject.SetActive(visible);
}
public void ShowMainMenuScreen(){ public void ShowMainMenuScreen(){
statsPanel.SetActive(true); SetStatsPanelVisible(true);
mainMenuScreen.SetActive(true); mainMenuScreen.SetActive(true);
gameModesScreen.SetActive(false); gameModesScreen.SetActive(false);
dummyBuyScreen.SetActive(false); dummyBuyScreen.SetActive(false);
withdrawalScreen.SetActive(false); withdrawalScreen.SetActive(false);
settingsScreen.SetActive(false);
} }
public void ShowGameModesScreen(){ public void ShowGameModesScreen(){
statsPanel.SetActive(true); SetStatsPanelVisible(true);
mainMenuScreen.SetActive(false); mainMenuScreen.SetActive(false);
gameModesScreen.SetActive(true); gameModesScreen.SetActive(true);
dummyBuyScreen.SetActive(false); dummyBuyScreen.SetActive(false);
withdrawalScreen.SetActive(false); withdrawalScreen.SetActive(false);
settingsScreen.SetActive(false);
} }
public void ShowDummyBuyScreen(){ public void ShowDummyBuyScreen(){
statsPanel.SetActive(false); SetStatsPanelVisible(false);
mainMenuScreen.SetActive(false); mainMenuScreen.SetActive(false);
gameModesScreen.SetActive(false); gameModesScreen.SetActive(false);
dummyBuyScreen.SetActive(true); dummyBuyScreen.SetActive(true);
withdrawalScreen.SetActive(false); withdrawalScreen.SetActive(false);
settingsScreen.SetActive(false);
} }
public void ShowWithdrawalScreen(){ public void ShowWithdrawalScreen(){
statsPanel.SetActive(false); SetStatsPanelVisible(false);
mainMenuScreen.SetActive(false); mainMenuScreen.SetActive(false);
gameModesScreen.SetActive(false); gameModesScreen.SetActive(false);
dummyBuyScreen.SetActive(false); dummyBuyScreen.SetActive(false);
withdrawalScreen.SetActive(true); withdrawalScreen.SetActive(true);
UpdateWithdrawalStatus(); settingsScreen.SetActive(false);
}
public void ShowSettingsScreen(){
SetStatsPanelVisible(false);
mainMenuScreen.SetActive(false);
gameModesScreen.SetActive(false);
dummyBuyScreen.SetActive(false);
withdrawalScreen.SetActive(false);
settingsScreen.SetActive(true);
RefreshSettingsProfile();
}
void RefreshSettingsProfile()
{
if (profileInfoFetcher == null)
return;
UserData user = LoginManager.CurrentUser;
if (user == null)
return;
profileInfoFetcher.GetPlayerProfile(user.id, (success, error, profile) =>
{
if (!success || profile == null)
{
Debug.LogWarning("Settings profile fetch failed: " + (error ?? "unknown"));
return;
}
ApplySettingsProfile(profile);
});
}
void ApplySettingsProfile(PlayerProfileApiResponse profile)
{
if (txtUsername != null)
txtUsername.text = profile.username ?? "";
if (txtEmail != null)
txtEmail.text = profile.email ?? "";
if (txtId != null)
txtId.text = profile.player_id.ToString();
if (txtAge != null)
txtAge.text = profile.days_since_registration.ToString();
if (txtMatches != null)
txtMatches.text = profile.matches_played.ToString();
if (txtWinRatio != null)
txtWinRatio.text = (profile.win_ratio * 100f).ToString("0.#") + "%";
} }
} }
+65 -4
View File
@@ -17,9 +17,11 @@ public class NetManager : NetworkManager
static bool s_IntentionalClientDisconnect; static bool s_IntentionalClientDisconnect;
public static NetManager instance;
public override void Awake() public override void Awake()
{ {
base.Awake(); base.Awake();
instance = this;
Transport.active.OnClientError += OnTransportClientError; Transport.active.OnClientError += OnTransportClientError;
} }
@@ -27,6 +29,35 @@ public class NetManager : NetworkManager
{ {
if (Transport.active != null) if (Transport.active != null)
Transport.active.OnClientError -= OnTransportClientError; Transport.active.OnClientError -= OnTransportClientError;
if (NetworkServer.active || NetworkClient.active)
StopNetworkingForSceneChange();
}
/// <summary>
/// Fully tears down Mirror before loading another scene. Required for tutorial host mode;
/// <see cref="NetworkManager.StopClient"/> alone leaves the server running.
/// </summary>
public static void StopNetworkingForSceneChange()
{
MarkIntentionalClientDisconnect();
NetworkManager manager = singleton != null ? singleton : NetworkManager.singleton;
if (manager != null)
{
if (NetworkServer.active && NetworkClient.active)
manager.StopHost();
else if (NetworkClient.active)
manager.StopClient();
else if (NetworkServer.active)
manager.StopServer();
return;
}
if (NetworkClient.active)
NetworkClient.Shutdown();
if (NetworkServer.active)
NetworkServer.Shutdown();
} }
static void OnTransportClientError(TransportError error, string reason) static void OnTransportClientError(TransportError error, string reason)
@@ -58,6 +89,13 @@ public class NetManager : NetworkManager
public override void OnServerDisconnect(NetworkConnectionToClient conn) public override void OnServerDisconnect(NetworkConnectionToClient conn)
{ {
if (conn != null && conn.identity != null)
{
var netPlayer = conn.identity.GetComponent<NetPlayer>();
if (netPlayer != null && GameManager.instance != null)
GameManager.instance.OnDedicatedMatchPlayerDisconnected(netPlayer.myTeam);
}
if (conn != null) if (conn != null)
Logger.Log($"Mirror server: client disconnected (connId={conn.connectionId})"); Logger.Log($"Mirror server: client disconnected (connId={conn.connectionId})");
base.OnServerDisconnect(conn); base.OnServerDisconnect(conn);
@@ -114,6 +152,7 @@ public class NetManager : NetworkManager
{ {
Application.logMessageReceived -= ForwardMirrorServerLogToLogger; Application.logMessageReceived -= ForwardMirrorServerLogToLogger;
ReplayRecorder.StopAndFlush();
GameManager.ReportDedicatedMatchRoomClosed(); GameManager.ReportDedicatedMatchRoomClosed();
base.OnStopServer(); base.OnStopServer();
} }
@@ -131,7 +170,10 @@ public class NetManager : NetworkManager
{ {
base.OnClientDisconnect(); base.OnClientDisconnect();
Logger.Log("Client disconnected"); Logger.Log("Client disconnected");
if (ShouldReconnect()) // Mirror raises OnDisconnectedEvent before clearing ConnectState, so
// NetworkClient.isConnected is still true here on involuntary drops.
// Gate on policy flags only; teardown wait happens in the reconnect loop.
if (CanAttemptReconnect())
{ {
if (reconnectCoroutine == null) if (reconnectCoroutine == null)
reconnectCoroutine = StartCoroutine(ReconnectLoopCoroutine()); reconnectCoroutine = StartCoroutine(ReconnectLoopCoroutine());
@@ -175,6 +217,12 @@ public class NetManager : NetworkManager
SetReconnectCanvasVisible(true); SetReconnectCanvasVisible(true);
reopenRequestedThisDisconnect = false; reopenRequestedThisDisconnect = false;
reopenRequestInFlight = false; reopenRequestInFlight = false;
// Wait until Mirror finishes OnClientDisconnectInternal (clears Connected
// then Shutdown). Without this, ShouldReconnect() fails immediately.
while (NetworkClient.isConnected || NetworkClient.active)
yield return null;
float reconnectStartedAt = Time.realtimeSinceStartup; float reconnectStartedAt = Time.realtimeSinceStartup;
float initialWaitEnd = Time.realtimeSinceStartup + InitialReconnectDelaySeconds; float initialWaitEnd = Time.realtimeSinceStartup + InitialReconnectDelaySeconds;
while (ShouldReconnect() && Time.realtimeSinceStartup < initialWaitEnd) while (ShouldReconnect() && Time.realtimeSinceStartup < initialWaitEnd)
@@ -211,20 +259,32 @@ public class NetManager : NetworkManager
SetReconnectCanvasVisible(false); SetReconnectCanvasVisible(false);
} }
bool ShouldReconnect() /// <summary>
/// Policy checks that are safe during Mirror's disconnect callback
/// (when <see cref="NetworkClient.isConnected"/> may still be true).
/// </summary>
bool CanAttemptReconnect()
{ {
if (s_IntentionalClientDisconnect) if (s_IntentionalClientDisconnect)
return false; return false;
if (NetworkServer.active) if (NetworkServer.active)
return false; return false;
if (NetworkClient.isConnected)
return false;
if (GameManager.instance != null && GameManager.instance.isGameEnded) if (GameManager.instance != null && GameManager.instance.isGameEnded)
return false; return false;
return true; return true;
} }
bool ShouldReconnect()
{
if (!CanAttemptReconnect())
return false;
if (NetworkClient.isConnected)
return false;
return true;
}
IEnumerator CoRequestReopenForCurrentPort() IEnumerator CoRequestReopenForCurrentPort()
{ {
reopenRequestInFlight = true; reopenRequestInFlight = true;
@@ -279,4 +339,5 @@ public class NetManager : NetworkManager
reopenRequestInFlight = false; reopenRequestInFlight = false;
} }
} }
+64 -11
View File
@@ -8,6 +8,7 @@ using UnityEngine.InputSystem;
#endif #endif
public class NetPlayer : NetworkBehaviour public class NetPlayer : NetworkBehaviour
{ {
const float PingReportIntervalSeconds = 30f;
[SyncVar] [SyncVar]
public int userId; public int userId;
@@ -36,6 +37,9 @@ public class NetPlayer : NetworkBehaviour
SetMyData(GameManager.MyTeam == Team.Red ? GameManager.RedPlayer.UserId : GameManager.BluePlayer.UserId); SetMyData(GameManager.MyTeam == Team.Red ? GameManager.RedPlayer.UserId : GameManager.BluePlayer.UserId);
#endif #endif
TeamSpecificEffects.instance.SetTeamSpecificEffects(myTeam); TeamSpecificEffects.instance.SetTeamSpecificEffects(myTeam);
if (GameManager.instance != null)
GameManager.instance.OnLocalPlayerJoinedMatch();
} }
void SetMyData(int uid){ void SetMyData(int uid){
@@ -96,6 +100,7 @@ public class NetPlayer : NetworkBehaviour
bool isDragging; bool isDragging;
Vector2 lastDragScreenPosition; Vector2 lastDragScreenPosition;
bool serverDragActive; bool serverDragActive;
Coroutine pingReportCoroutine;
#region EMOTES #region EMOTES
public void SendTextEmote(string txtName){ public void SendTextEmote(string txtName){
@@ -155,10 +160,16 @@ public class NetPlayer : NetworkBehaviour
if(isLocalPlayer){ if(isLocalPlayer){
localPlayer = this; localPlayer = this;
SetupInputPanel(); SetupInputPanel();
pingReportCoroutine = StartCoroutine(CoReportPingPeriodically());
} }
} }
void OnDestroy(){ void OnDestroy(){
if (pingReportCoroutine != null)
{
StopCoroutine(pingReportCoroutine);
pingReportCoroutine = null;
}
if(localPlayer == this){ if(localPlayer == this){
localPlayer = null; localPlayer = null;
} }
@@ -419,29 +430,30 @@ public class NetPlayer : NetworkBehaviour
/// Request a rematch with the opponent. /// Request a rematch with the opponent.
/// </summary> /// </summary>
public void RequestRematch(){ public void RequestRematch(){
int maxBetLimitCoins = GameOverCanvas.GetPersonalBetLimit();
if(isServer){ if(isServer){
GameOverCanvas.instance.OnRematchRequested(); GameOverCanvas.instance.OnRematchRequested(maxBetLimitCoins);
RpcRequestRematch(); RpcRequestRematch(maxBetLimitCoins);
}else{ }else{
CmdRequestRematch(); CmdRequestRematch(maxBetLimitCoins);
} }
} }
[Command] [Command]
void CmdRequestRematch(){ void CmdRequestRematch(int maxBetLimitCoins){
Logger.Log("Rematch requested"); Logger.Log("Rematch requested, max bet limit coins: " + maxBetLimitCoins);
if(isServerOnly){ if(isServerOnly){
//server //server
}else{ }else{
GameOverCanvas.instance.OnRematchRequested(); GameOverCanvas.instance.OnRematchRequested(maxBetLimitCoins);
} }
RpcRequestRematch(); RpcRequestRematch(maxBetLimitCoins);
} }
[ClientRpc] [ClientRpc]
void RpcRequestRematch(){ void RpcRequestRematch(int maxBetLimitCoins){
GameOverCanvas.instance.OnRematchRequested(); GameOverCanvas.instance.OnRematchRequested(maxBetLimitCoins);
} }
@@ -474,6 +486,14 @@ public class NetPlayer : NetworkBehaviour
Logger.Log(matchmadeResponseString); Logger.Log(matchmadeResponseString);
StartCoroutine(CoroutineOnRematchConfirmedServer()); StartCoroutine(CoroutineOnRematchConfirmedServer());
} }
else
{
string error = string.IsNullOrEmpty(resp.ErrorMessage)
? "Could not start a rematch."
: resp.ErrorMessage;
Logger.Log("Rematch failed: " + error);
RpcRematchFailed(error);
}
} }
IEnumerator CoroutineOnRematchConfirmedServer(){ IEnumerator CoroutineOnRematchConfirmedServer(){
@@ -530,8 +550,16 @@ public class NetPlayer : NetworkBehaviour
Cupid.RoomPort = branch.Port; Cupid.RoomPort = branch.Port;
LoginManager.SetMatchYourTeam(branch.your_team); LoginManager.SetMatchYourTeam(branch.your_team);
GameManager.instance.StopClient(); LevelLoadManager.instance.Leave("Game");
LevelLoadManager.LoadLevel("Game"); }
[ClientRpc]
void RpcRematchFailed(string errorMessage)
{
if (GameOverCanvas.instance != null)
GameOverCanvas.instance.OnRematchFailed(errorMessage);
else
MessageBoxDialog.Show("Rematch failed", string.IsNullOrEmpty(errorMessage) ? "Could not start a rematch." : errorMessage);
} }
void OnRematchCancelledServer(){ void OnRematchCancelledServer(){
@@ -552,4 +580,29 @@ public class NetPlayer : NetworkBehaviour
GameOverCanvas.instance.OnRematchCancelled(); GameOverCanvas.instance.OnRematchCancelled();
} }
IEnumerator CoReportPingPeriodically()
{
var wait = new WaitForSecondsRealtime(PingReportIntervalSeconds);
while (isLocalPlayer)
{
yield return wait;
if (!NetworkClient.active || !NetworkClient.isConnected)
continue;
if (LoginManager.instance == null || string.IsNullOrEmpty(LoginManager.AuthToken))
continue;
int matchId = GameManager.DedicatedMatchId;
if (matchId <= 0)
continue;
int pingMs = Mathf.Max(0, Mathf.RoundToInt((float)(NetworkTime.rtt * 1000.0)));
LoginManager.instance.ReportPing(matchId, pingMs, (ok, err, report) =>
{
if (!ok && !string.IsNullOrEmpty(err))
Logger.Log("Ping report failed: " + err);
});
}
}
} }
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
public class ProfileInfoFetcher : MonoBehaviour
{
/// <summary>GET /players/:playerId/profile. Callback (success, errorOrNull, profileOrNull).</summary>
public void GetPlayerProfile(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
{
StartCoroutine(FetchPlayerProfileCoroutine(playerId, onComplete));
}
IEnumerator FetchPlayerProfileCoroutine(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
{
string url = LoginManager.AuthBaseUrl + "/players/" + playerId + "/profile";
using (var request = UnityWebRequest.Get(url))
{
request.downloadHandler = new DownloadHandlerBuffer();
if (!string.IsNullOrEmpty(LoginManager.AuthToken))
request.SetRequestHeader("Authorization", "Bearer " + LoginManager.AuthToken);
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
yield break;
}
if (string.IsNullOrEmpty(text))
{
onComplete?.Invoke(false, "Empty response from server", null);
yield break;
}
PlayerProfileApiResponse resp = JsonUtility.FromJson<PlayerProfileApiResponse>(text);
if (resp != null && resp.ok)
onComplete?.Invoke(true, null, resp);
else
{
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Request failed";
onComplete?.Invoke(false, err, null);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5e6e403cd7bcf0149bad56dbad78c302
+74 -3
View File
@@ -6,6 +6,7 @@ using UnityEngine.EventSystems;
public class Puck : NetworkBehaviour public class Puck : NetworkBehaviour
{ {
[HideInInspector]public Rigidbody2D rb; [HideInInspector]public Rigidbody2D rb;
Collider2D col;
[SyncVar] [SyncVar]
public bool isSelected; public bool isSelected;
public LineRenderer lineRenderer; public LineRenderer lineRenderer;
@@ -30,6 +31,7 @@ public class Puck : NetworkBehaviour
UpdateTeam(); UpdateTeam();
markerStartScale = selectedMarker.localScale; markerStartScale = selectedMarker.localScale;
rb=GetComponent<Rigidbody2D>(); rb=GetComponent<Rigidbody2D>();
col = GetComponent<Collider2D>();
startPosition = transform.position; startPosition = transform.position;
} }
@@ -52,6 +54,8 @@ public class Puck : NetworkBehaviour
GameEvents.OnSelectedPuckChanged -= OnSelectedPuckChanged; GameEvents.OnSelectedPuckChanged -= OnSelectedPuckChanged;
GameManager.OnTeamChanged -= OnTeamChanged; GameManager.OnTeamChanged -= OnTeamChanged;
RestoreAfterReset();
if (GameManager.instance != null)
GameManager.instance.DisposePuck(this); GameManager.instance.DisposePuck(this);
} }
public Vector3 lineEnd; public Vector3 lineEnd;
@@ -135,6 +139,7 @@ public class Puck : NetworkBehaviour
if (AudioManager.instance != null){ if (AudioManager.instance != null){
AudioManager.instance.PlayPuckHit(vol); AudioManager.instance.PlayPuckHit(vol);
if(isServer){ if(isServer){
ReplayRecorder.RecordHit("puck_puck", vol, this);
RpcPuckHitAudio(vol); RpcPuckHitAudio(vol);
} }
} }
@@ -145,27 +150,64 @@ public class Puck : NetworkBehaviour
AudioManager.instance.PlayPuckHit(vol); AudioManager.instance.PlayPuckHit(vol);
} }
Coroutine coroutineReset;
bool holdingCollisionLock;
public void Reset(Vector3? position = null, float duration = 0.5f) public void Reset(Vector3? position = null, float duration = 0.5f)
{ {
UnscheduleReset();
Vector3 pos = position ?? startPosition; Vector3 pos = position ?? startPosition;
StartCoroutine(CoroutineReset(pos, duration)); if (coroutineReset != null)
{
StopCoroutine(coroutineReset);
RestoreAfterReset();
}
coroutineReset = StartCoroutine(CoroutineReset(pos, duration));
}
public void SetCollisionsEnabled(bool enabled)
{
if (col != null)
col.enabled = enabled;
if (rb != null && !enabled)
{
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
}
} }
Coroutine coroutineScheduleReset; Coroutine coroutineScheduleReset;
public void ScheduleReset(Vector3 position, float duration = 0.5f){ public void ScheduleReset(Vector3 position, float duration = 0.5f){
UnscheduleReset();
coroutineScheduleReset = StartCoroutine(CoroutineScheduleReset(position, duration)); coroutineScheduleReset = StartCoroutine(CoroutineScheduleReset(position, duration));
} }
public void UnscheduleReset(){ public void UnscheduleReset(){
if(coroutineScheduleReset != null){ if(coroutineScheduleReset != null){
StopCoroutine(coroutineScheduleReset); StopCoroutine(coroutineScheduleReset);
coroutineScheduleReset = null;
}
}
public void CancelInGoalReset()
{
UnscheduleReset();
if (coroutineReset != null)
{
StopCoroutine(coroutineReset);
RestoreAfterReset();
} }
} }
IEnumerator CoroutineScheduleReset(Vector3 position, float duration){ IEnumerator CoroutineScheduleReset(Vector3 position, float duration){
while(GameManager.isMoving){ while(GameManager.isMoving){
if (GameManager.instance != null && !GameManager.instance.CanProcessGoal)
yield break;
yield return null; yield return null;
} }
if (GameManager.instance != null && !GameManager.instance.CanProcessGoal)
yield break;
coroutineScheduleReset = null;
Debug.Log("Resetting puck", gameObject); Debug.Log("Resetting puck", gameObject);
Vector3 dir = position - transform.position; Vector3 dir = position - transform.position;
Vector3 centerDir = Vector3.zero - transform.position; Vector3 centerDir = Vector3.zero - transform.position;
@@ -187,15 +229,44 @@ public class Puck : NetworkBehaviour
} }
IEnumerator CoroutineReset(Vector3 pos, float duration){ IEnumerator CoroutineReset(Vector3 pos, float duration){
if (GameManager.instance != null)
{
GameManager.instance.PushResetCollisionLock();
holdingCollisionLock = true;
}
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
float t=0; float t=0;
Vector3 pos1 = transform.position; Vector3 pos1 = transform.position;
Quaternion rot1 = transform.rotation; Quaternion rot1 = transform.rotation;
while(t<duration){ while(t<duration){
t+=Time.deltaTime; t+=Time.deltaTime;
transform.position = Vector3.Lerp(pos1, pos, t/duration); float u = Mathf.Clamp01(t/duration);
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, t/duration); transform.position = Vector3.Lerp(pos1, pos, u);
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
yield return null; yield return null;
} }
transform.position = pos;
transform.rotation = Quaternion.identity;
RestoreAfterReset();
}
void RestoreAfterReset()
{
if (rb != null)
{
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
}
if (holdingCollisionLock && GameManager.instance != null)
{
GameManager.instance.PopResetCollisionLock();
holdingCollisionLock = false;
}
coroutineReset = null;
} }
} }
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9cff4a867ddc05341bf42777b654509a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+291
View File
@@ -0,0 +1,291 @@
# Match Replay JSON Format
Guide for implementing the admin-panel replay viewer. Files are written by the Unity dedicated server (and practice host) to disk only.
## Where files live
Written next to the match log with the **same basename**, only the extension differs:
```
{ApplicationDirectory}/Logs/{matchId}.txt ← match log
{ApplicationDirectory}/Logs/{matchId}.json ← replay
```
| Match type | `matchId` | Example pair |
|------------|-----------|--------------|
| Ranked / dedicated | Positive matchmaker id | `Logs/228.txt` + `Logs/228.json` |
| Practice / tutorial | Negative unix timestamp | `Logs/-1769539200.txt` + `Logs/-1769539200.json` |
Admin panel: given a log at `Logs/{id}.txt`, load `Logs/{id}.json` beside it.
## Top-level object
```json
{
"version": 1,
"matchId": 228,
"isPractice": false,
"fixedDeltaTime": 0.02,
"duration": 184.32,
"entities": [ /* ... */ ],
"frames": [ /* ... */ ],
"events": [ /* ... */ ]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `version` | int | Schema version. Currently `1`. |
| `matchId` | int | Match id (positive = ranked, negative = practice). |
| `isPractice` | bool | `true` for localhost practice / tutorial host. |
| `fixedDeltaTime` | float | Physics step used while recording (typically `0.02` → ~50 Hz). |
| `duration` | float | Length of the recording in seconds (`t` of last sample / end). |
| `entities` | array | Ball + pucks. Order defines pose array indices in every frame. |
| `frames` | array | Time-ordered pose samples (~every FixedUpdate). |
| `events` | array | Time-ordered discrete events (hits, goals, resets). |
Time `t` everywhere is **seconds since recording start** (match start), not wall-clock UTC.
---
## Entities
```json
{ "id": 0, "type": "ball", "team": "" }
{ "id": 1, "type": "puck", "team": "Red" }
{ "id": 2, "type": "puck", "team": "Blue" }
```
| Field | Type | Values |
|-------|------|--------|
| `id` | int | Stable index `0 .. N-1`. Matches array index in each frames `x`/`y`/`vx`/`vy`. |
| `type` | string | `"ball"` or `"puck"` |
| `team` | string | `"Red"`, `"Blue"`, or `""` for ball |
Entity list is fixed for the whole file. **Index `i` in a frames pose arrays always refers to `entities[i]`.**
Typical order: ball first, then pucks sorted by team then spawn position.
---
## Frames (motion samples)
```json
{
"t": 12.40,
"x": [0.0, 1.2, -0.5],
"y": [0.1, -0.3, 2.0],
"vx": [0.0, 3.1, -1.0],
"vy": [0.2, 0.0, 0.5]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `t` | float | Sample time in seconds (monotonic, increasing). |
| `x`, `y` | float[] | World position per entity (Unity 2D world units). |
| `vx`, `vy` | float[] | Linear velocity per entity at that sample (world units / second). |
- Array length always equals `entities.length`.
- Samples are dense (~`1 / fixedDeltaTime` Hz). Do **not** treat them as render frames.
### Coordinate system
Unity 2D world space:
- Origin at field center
- `+X` right, `+Y` up (as in Unity)
- Scale matches the in-game pitch (see game `fieldSize` / art for admin canvas mapping)
---
## Events
```json
{ "t": 12.4, "type": "hit", "kind": "ball_wall", "vol": 0.6, "id": 0, "team": "", "redScore": 0, "blueScore": 0, "kickoff": false }
{ "t": 30.1, "type": "goal", "kind": "", "vol": 0, "id": -1, "team": "Blue", "redScore": 1, "blueScore": 0, "kickoff": false }
{ "t": 32.2, "type": "reset", "kind": "", "vol": 0, "id": -1, "team": "", "redScore": 0, "blueScore": 0, "kickoff": false }
```
All event objects share the same fields (Unity `JsonUtility` shape). Unused fields are empty / zero / `-1`.
| Field | Used by | Description |
|-------|---------|-------------|
| `t` | all | Event time in seconds. |
| `type` | all | `"hit"` \| `"goal"` \| `"reset"` |
| `kind` | `hit` | `"puck_puck"` \| `"ball_puck"` \| `"ball_wall"` |
| `vol` | `hit` | SFX volume `0..1` (from relative impact speed). |
| `id` | `hit` | Entity id involved (spatial cue). `-1` if unknown. |
| `team` | `goal` | **Conceding** side / goal owner (`"Red"` or `"Blue"`). The other team scored. |
| `redScore` | `goal` | Score after this goal. |
| `blueScore` | `goal` | Score after this goal. |
| `kickoff` | `goal`, `reset` | Kickoff-goal / kickoff-style reset when `true`. |
### Event semantics
| `type` | Meaning for the viewer |
|--------|-------------------------|
| `hit` | Play collision SFX at `vol`. Optional: flash near entity `id`. |
| `goal` | Goal scored against `team`. Update scoreboard to `redScore`/`blueScore`. Play goal SFX. **Discontinuity** — do not interpolate motion across this time. |
| `reset` | Pieces are lerping / teleporting back. **Discontinuity** — snap poses; do not blend across. |
First-to-3 wins; a completed match usually ends shortly after a goal that makes a score `3`.
---
## Playback contract (required for smooth motion)
Recording is FixedUpdate (~50 Hz). Snapping happens if the viewer steps sample-by-sample without blending. Follow this:
### 1. Drive time with wall clock
```text
playbackTime += (deltaMs / 1000) * playbackSpeed
playbackTime = clamp(playbackTime, 0, duration)
```
Use `requestAnimationFrame` (or equivalent). Do **not** advance one JSON frame per rAF tick.
### 2. Interpolate poses between samples
Binary-search `frames` for neighbors where:
```text
frames[i].t <= playbackTime < frames[i+1].t
```
Then for each entity index `e`:
**Minimum (good):** linear lerp of `x`/`y`:
```text
u = (playbackTime - t0) / (t1 - t0)
x = lerp(x0[e], x1[e], u)
y = lerp(y0[e], y1[e], u)
```
**Recommended (smooth under acceleration):** cubic Hermite using velocities:
```text
dt = t1 - t0
u = (playbackTime - t0) / dt
u2 = u*u
u3 = u2*u
h00 = 2*u3 - 3*u2 + 1
h10 = u3 - 2*u2 + u
h01 = -2*u3 + 3*u2
h11 = u3 - u2
x = h00*x0 + h10*dt*vx0 + h01*x1 + h11*dt*vx1
y = h00*y0 + h10*dt*vy0 + h01*y1 + h11*dt*vy1
```
(Same for each entity index.)
### 3. Snap across discontinuities
Treat every `goal` and `reset` event as a cut:
- When `playbackTime` crosses that events `t`, **do not** interpolate from the previous sample across the cut.
- Seek to the frame at or immediately after `t` and snap positions.
Practical approach: build a sorted list of discontinuity times from events where `type === "goal" || type === "reset"`. If `t0` and `t1` straddle a discontinuity, snap to the post-cut sample instead of lerping.
### 4. Fire events once when crossed
Keep `lastEventIndex` (or last fired `t`). When `playbackTime` advances past an events `t`, fire it once:
- `hit` → SFX by `kind` / `vol`
- `goal` → score UI + goal audio
- `reset` → optional reset cue (usually silent)
On seek / scrub: reset event cursor; either skip SFX or replay only from the new time forward.
---
## Suggested viewer pipeline
```text
Load JSON
→ map entities to sprites (ball / red pucks / blue pucks)
→ sort events by t (already ordered, but verify)
→ each animation frame:
advance playbackTime
resolve poses (Hermite + discontinuity snaps)
draw entities
dispatch newly crossed events
```
### Seeking / scrubbing
1. Set `playbackTime` to the scrub value.
2. Find the nearest frame (or interpolate as above).
3. Recompute score from the last `goal` event with `t <= playbackTime` (else `00`).
4. Reset the event-fire cursor to that time so SFX dont spam.
### Scoreboard without scrubbing
Start at `00`. On each `goal` event, set scores from that events `redScore` / `blueScore`.
---
## Minimal TypeScript types
```ts
export interface ReplayFile {
version: number;
matchId: number;
isPractice: boolean;
fixedDeltaTime: number;
duration: number;
entities: ReplayEntity[];
frames: ReplayFrame[];
events: ReplayEvent[];
}
export interface ReplayEntity {
id: number;
type: "ball" | "puck" | string;
team: "Red" | "Blue" | "" | string;
}
export interface ReplayFrame {
t: number;
x: number[];
y: number[];
vx: number[];
vy: number[];
}
export interface ReplayEvent {
t: number;
type: "hit" | "goal" | "reset" | string;
kind: "puck_puck" | "ball_puck" | "ball_wall" | "" | string;
vol: number;
id: number; // entity id, or -1
team: "Red" | "Blue" | "" | string; // conceding team on goal
redScore: number;
blueScore: number;
kickoff: boolean;
}
```
---
## Checklist for a correct viewer
- [ ] Time driven by real elapsed time × speed, not frame index
- [ ] Poses interpolated between samples (Hermite preferred)
- [ ] No interpolation across `goal` / `reset`
- [ ] Entity index `i` always matches `entities[i]`
- [ ] Hit SFX use `kind` + `vol`
- [ ] Goal `team` means **conceding** side
- [ ] Practice files filtered via `isPractice` or negative `matchId`
## Out of scope (not in the JSON)
- Player names / user ids (use matchmaker / match API by `matchId` for ranked games)
- Camera / UI layout
- Launch aim lines / turn timer
- Network latency or client prediction
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8cced32e9b154814b99db72a5ae58c70
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+61
View File
@@ -0,0 +1,61 @@
using System;
/// <summary>
/// Replay JSON DTOs for admin-panel playback.
/// Playback contract:
/// 1. Advance playbackTime with real time (scaled).
/// 2. Binary-search frames by t; Hermite-interpolate x/y using vx/vy between neighbors.
/// 3. On reset/goal events (discontinuities): snap to the sample at/after t — do not blend across.
/// 4. Fire SFX on hit/goal when playbackTime crosses event t.
/// </summary>
[Serializable]
public class ReplayFile
{
public int version = 1;
public int matchId;
/// <summary>True for localhost practice / tutorial host matches (no dedicated matchmaker id).</summary>
public bool isPractice;
public float fixedDeltaTime;
public float duration;
public ReplayEntity[] entities;
public ReplayFrame[] frames;
public ReplayEvent[] events;
}
[Serializable]
public class ReplayEntity
{
public int id;
/// <summary>"ball" or "puck"</summary>
public string type;
/// <summary>"Red", "Blue", or empty for ball</summary>
public string team;
}
[Serializable]
public class ReplayFrame
{
public float t;
public float[] x;
public float[] y;
public float[] vx;
public float[] vy;
}
[Serializable]
public class ReplayEvent
{
public float t;
/// <summary>"hit", "goal", or "reset"</summary>
public string type;
/// <summary>For hits: "puck_puck", "ball_puck", or "ball_wall"</summary>
public string kind;
public float vol;
/// <summary>Entity id for spatial cueing; -1 when N/A</summary>
public int id = -1;
/// <summary>For goals: conceding team ("Red"/"Blue")</summary>
public string team;
public int redScore;
public int blueScore;
public bool kickoff;
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2c3d4e5f60718293a4b5c6d7e8f901a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+301
View File
@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.IO;
using Mirror;
using UnityEngine;
/// <summary>
/// Server-only match recorder. Samples poses every FixedUpdate; flushes JSON to disk on stop.
/// Admin playback must interpolate by time (Hermite using velocity) — never snap one FixedUpdate sample per render frame.
/// </summary>
public class ReplayRecorder : MonoBehaviour
{
public static ReplayRecorder Instance { get; private set; }
public static bool IsRecording => Instance != null && Instance._recording && !Instance._flushed;
bool _recording;
bool _flushed;
int _matchId;
bool _isPractice;
float _startFixedTime;
float _duration;
readonly List<ReplayEntity> _entities = new List<ReplayEntity>();
readonly List<Rigidbody2D> _bodies = new List<Rigidbody2D>();
readonly Dictionary<int, int> _instanceIdToEntityId = new Dictionary<int, int>();
readonly List<ReplayFrame> _frames = new List<ReplayFrame>();
readonly List<ReplayEvent> _events = new List<ReplayEvent>();
/// <returns>True if recording started (or already recording this session).</returns>
public static bool StartRecording(int matchId, GameManager gm)
{
if (!NetworkServer.active || gm == null)
return false;
if (Instance == null)
{
var go = new GameObject("ReplayRecorder");
DontDestroyOnLoad(go);
Instance = go.AddComponent<ReplayRecorder>();
}
Instance.Begin(matchId, gm);
return true;
}
public static void StopAndFlush()
{
if (Instance != null)
Instance.Flush();
}
public static void RecordHit(string kind, float vol, Component source)
{
if (!IsRecording)
return;
Instance.AddHit(kind, vol, source);
}
public static void RecordGoal(Team concedingTeam, int redScore, int blueScore, bool kickoff)
{
if (!IsRecording)
return;
Instance.AddEvent(new ReplayEvent
{
t = Instance.CurrentT(),
type = "goal",
kind = "",
vol = 0f,
id = -1,
team = concedingTeam.ToString(),
redScore = redScore,
blueScore = blueScore,
kickoff = kickoff
});
}
public static void RecordReset(bool kickoff)
{
if (!IsRecording)
return;
Instance.AddEvent(new ReplayEvent
{
t = Instance.CurrentT(),
type = "reset",
kind = "",
vol = 0f,
id = -1,
team = "",
redScore = 0,
blueScore = 0,
kickoff = kickoff
});
}
public static bool TryGetEntityId(Component source, out int entityId)
{
entityId = -1;
if (Instance == null || source == null)
return false;
return Instance._instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
}
void Begin(int matchId, GameManager gm)
{
if (_recording && !_flushed)
Flush();
// Practice / tutorial host uses DedicatedMatchId 0 — mint a unique id so files don't overwrite.
_isPractice = matchId <= 0 || GameTutorialManager.tutorialModeEnabled;
if (matchId <= 0)
matchId = GeneratePracticeMatchId();
_matchId = matchId;
_recording = true;
_flushed = false;
_startFixedTime = Time.fixedTime;
_duration = 0f;
_entities.Clear();
_bodies.Clear();
_instanceIdToEntityId.Clear();
_frames.Clear();
_events.Clear();
// Keep log + replay siblings: Logs/{matchId}.txt and Logs/{matchId}.json
if (_isPractice)
Logger.SetFileName(_matchId.ToString());
BuildEntities(gm);
string mode = _isPractice ? "practice" : "match";
Logger.Log($"ReplayRecorder started ({mode}) id {_matchId} with {_entities.Count} entities");
}
/// <summary>Negative unix timestamp so practice ids never collide with positive matchmaker ids.</summary>
static int GeneratePracticeMatchId()
{
long unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (unix > int.MaxValue)
unix %= int.MaxValue;
int id = -(int)unix;
return id == 0 ? -1 : id;
}
void BuildEntities(GameManager gm)
{
if (gm.ball != null)
{
RegisterEntity(gm.ball.gameObject, gm.ball, "ball", "");
}
var sorted = new List<Puck>(gm.pucks);
sorted.Sort((a, b) =>
{
int teamCmp = a.team.CompareTo(b.team);
if (teamCmp != 0)
return teamCmp;
int xCmp = a.transform.position.x.CompareTo(b.transform.position.x);
if (xCmp != 0)
return xCmp;
return a.transform.position.y.CompareTo(b.transform.position.y);
});
foreach (Puck puck in sorted)
{
if (puck == null || puck.rb == null)
continue;
RegisterEntity(puck.gameObject, puck.rb, "puck", puck.team.ToString());
}
}
void RegisterEntity(GameObject go, Rigidbody2D rb, string type, string team)
{
int id = _entities.Count;
_entities.Add(new ReplayEntity { id = id, type = type, team = team ?? "" });
_bodies.Add(rb);
_instanceIdToEntityId[go.GetInstanceID()] = id;
}
float CurrentT() => Time.fixedTime - _startFixedTime;
void FixedUpdate()
{
if (!_recording || _flushed || !NetworkServer.active)
return;
int n = _bodies.Count;
if (n == 0)
return;
var frame = new ReplayFrame
{
t = CurrentT(),
x = new float[n],
y = new float[n],
vx = new float[n],
vy = new float[n]
};
for (int i = 0; i < n; i++)
{
Rigidbody2D rb = _bodies[i];
if (rb == null)
continue;
Vector2 pos = rb.position;
Vector2 vel = rb.linearVelocity;
frame.x[i] = pos.x;
frame.y[i] = pos.y;
frame.vx[i] = vel.x;
frame.vy[i] = vel.y;
}
_duration = frame.t;
_frames.Add(frame);
}
void AddHit(string kind, float vol, Component source)
{
int entityId = -1;
if (source != null)
_instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
AddEvent(new ReplayEvent
{
t = CurrentT(),
type = "hit",
kind = kind ?? "",
vol = vol,
id = entityId,
team = "",
redScore = 0,
blueScore = 0,
kickoff = false
});
}
void AddEvent(ReplayEvent ev)
{
_events.Add(ev);
}
void Flush()
{
if (!_recording || _flushed)
return;
_flushed = true;
_recording = false;
_duration = Mathf.Max(_duration, CurrentT());
var file = new ReplayFile
{
version = 1,
matchId = _matchId,
isPractice = _isPractice,
fixedDeltaTime = Time.fixedDeltaTime,
duration = _duration,
entities = _entities.ToArray(),
frames = _frames.ToArray(),
events = _events.ToArray()
};
try
{
string path = ResolveReplayFilePath();
string json = JsonUtility.ToJson(file);
File.WriteAllText(path, json);
Logger.Log($"ReplayRecorder wrote {path} ({_frames.Count} frames, {_events.Count} events, duration {_duration:F2}s)");
}
catch (Exception e)
{
Logger.Log("ReplayRecorder flush failed: " + e.Message);
}
}
/// <summary>
/// Same directory and basename as the match log, extension <c>.json</c>
/// (e.g. <c>Logs/228.txt</c> → <c>Logs/228.json</c>).
/// </summary>
string ResolveReplayFilePath()
{
string expectedName = _matchId.ToString();
string logPath = Logger.instance != null ? Logger.instance.LogFilePath : null;
if (!string.IsNullOrEmpty(logPath))
{
string logBase = Path.GetFileNameWithoutExtension(logPath);
if (logBase == expectedName)
return Path.ChangeExtension(logPath, ".json");
}
return Path.Combine(Logger.GetLogsDirectory(), expectedName + ".json");
}
void OnDestroy()
{
if (Instance == this)
{
Flush();
Instance = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c3d4e5f60718293a4b5c6d7e8f901a2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1
View File
@@ -56,6 +56,7 @@ public class ServerAutoClose : MonoBehaviour
Logger.Log("All players left, exiting"); Logger.Log("All players left, exiting");
else else
Logger.Log("No players joined, exiting"); Logger.Log("No players joined, exiting");
// ReportDedicatedMatchRoomClosed settles /winner first when escrow is open.
GameManager.ReportDedicatedMatchRoomClosed(); GameManager.ReportDedicatedMatchRoomClosed();
Application.Quit(); Application.Quit();
} }
+27 -3
View File
@@ -31,6 +31,11 @@ public class TutorialManagerMainMenu : MonoBehaviour
[Header("Screen 6")] [Header("Screen 6")]
public GameObject screen6; public GameObject screen6;
public Button btnNext6;
public CardCarousel gameModeCarousel;
[Header("Screen 7")]
public GameObject screen7;
public Button btnFinish; public Button btnFinish;
void Start() void Start()
@@ -38,25 +43,28 @@ public class TutorialManagerMainMenu : MonoBehaviour
btnNext1.onClick.AddListener(OnNext1); btnNext1.onClick.AddListener(OnNext1);
btnSkip.onClick.AddListener(OnSkip); btnSkip.onClick.AddListener(OnSkip);
btnNext2.onClick.AddListener(OnNext2); btnNext2.onClick.AddListener(OnNext2);
btnNext6.onClick.AddListener(OnNext6);
btnFinish.onClick.AddListener(OnTutorialCompleted); btnFinish.onClick.AddListener(OnStartTutorialMatch);
if(!forceShow){ if(!forceShow){
if(PlayerPrefs.HasKey(TUTORIAL_PREF_KEY)){ if(PlayerPrefs.HasKey(TUTORIAL_PREF_KEY)){
tutorialPanel.SetActive(false); tutorialPanel.SetActive(false);
Debug.Log("Tutorial pref key is available, stopping;");
return; return;
} }
if(LoginManager.CurrentUser.rc > 0){ if(LoginManager.CurrentUser.rc > 0){
tutorialPanel.SetActive(false); tutorialPanel.SetActive(false);
Debug.Log("User has rc, stopping;");
return; return;
} }
} }
tutorialPanel.SetActive(true); tutorialPanel.SetActive(true);
screen1.SetActive(true); screen1.SetActive(true);
PlayerPrefs.SetInt(TUTORIAL_PREF_KEY, 1);
PlayerPrefs.Save();
} }
void OnNext1(){ void OnNext1(){
@@ -77,6 +85,15 @@ public class TutorialManagerMainMenu : MonoBehaviour
curScreen = 3; curScreen = 3;
} }
void OnNext6(){
screen6.SetActive(false);
screen7.SetActive(true);
gameModeCarousel.SetSelectedIndex(1);
curScreen = 7;
}
void OnNext3(){ void OnNext3(){
screen3.SetActive(false); screen3.SetActive(false);
screen4.SetActive(true); screen4.SetActive(true);
@@ -103,6 +120,13 @@ public class TutorialManagerMainMenu : MonoBehaviour
tutorialPanel.SetActive(false); tutorialPanel.SetActive(false);
} }
void OnStartTutorialMatch()
{
OnTutorialCompleted();
if (MainMenuManager.instance != null)
MainMenuManager.instance.OnPlayTutorial();
}
void Update() void Update()
{ {
if(curScreen < 2){return;} if(curScreen < 2){return;}
+229
View File
@@ -0,0 +1,229 @@
using System;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class MessageBoxDialog : MonoBehaviour
{
public const float ANIMATION_DURATION = 0.25f;
/// <summary>Above game/menu/loader canvases; below reconnect (500).</summary>
const int OverlaySortingOrder = 400;
public static MessageBoxDialog instance;
public static bool IsAvailable => instance != null;
public RectTransform dimmer;
public RectTransform basicMessageBox;
[Header("MessageBox")]
public RectTransform messageBox;
public TMP_Text txtTitle, txtMessage;
public Button btnOk, btnYes, btnNo;
Image dimmerImg;
TMP_Text basicMessageText;
Action<bool> onYesOrNo;
Sequence showHideSequence;
Canvas overlayCanvas;
void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
overlayCanvas = GetComponent<Canvas>();
if (overlayCanvas != null)
overlayCanvas.sortingOrder = OverlaySortingOrder;
if (dimmer != null)
dimmerImg = dimmer.GetComponent<Image>();
if (basicMessageBox != null)
basicMessageText = basicMessageBox.GetComponentInChildren<TMP_Text>();
}
void OnEnable()
{
if (instance != this)
return;
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDisable()
{
if (instance != this)
return;
SceneManager.sceneLoaded -= OnSceneLoaded;
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
transform.SetAsLastSibling();
}
public static void ShowBasic(string title)
{
if (instance == null)
{
Debug.LogWarning("MessageBoxDialog missing: " + title);
return;
}
instance.ShowMessageBox(title);
}
public static void Show(string title, string message, Action<bool> onResult = null)
{
if (instance == null)
{
Debug.LogWarning("MessageBoxDialog missing: " + title);
return;
}
instance.ShowMessageBox(title, message, onResult);
}
void OnDestroy()
{
KillTweens();
if (instance == this)
instance = null;
}
void Start()
{
Button basicButton = basicMessageBox != null ? basicMessageBox.GetComponent<Button>() : null;
if (basicButton != null)
basicButton.onClick.AddListener(HideMessageBox);
if (btnOk != null)
btnOk.onClick.AddListener(HideMessageBox);
if (btnYes != null)
btnYes.onClick.AddListener(() => OnQuestionDialog(true));
if (btnNo != null)
btnNo.onClick.AddListener(() => OnQuestionDialog(false));
}
void OnQuestionDialog(bool val)
{
Action<bool> callback = onYesOrNo;
onYesOrNo = null;
HideMessageBox();
callback?.Invoke(val);
}
public void ShowMessageBox(string title)
{
PrepareShow();
HideBoxImmediate(messageBox);
if (basicMessageText != null)
basicMessageText.text = title;
AnimateIn(basicMessageBox);
}
public void ShowMessageBox(string title, string message, Action<bool> onResult = null)
{
PrepareShow();
HideBoxImmediate(basicMessageBox);
txtTitle.text = title;
txtMessage.text = message;
bool isQuestion = onResult != null;
btnNo.gameObject.SetActive(isQuestion);
btnYes.gameObject.SetActive(isQuestion);
btnOk.gameObject.SetActive(!isQuestion);
onYesOrNo = onResult;
AnimateIn(messageBox);
}
public void HideMessageBox()
{
bool basicActive = basicMessageBox != null && basicMessageBox.gameObject.activeSelf;
bool fullActive = messageBox != null && messageBox.gameObject.activeSelf;
if (!basicActive && !fullActive)
return;
if (basicActive && fullActive)
HideBoxImmediate(basicMessageBox);
AnimateOut(basicActive && !fullActive ? basicMessageBox : messageBox);
}
void PrepareShow()
{
KillTweens();
onYesOrNo = null;
transform.SetAsLastSibling();
dimmer.gameObject.SetActive(true);
if (dimmerImg != null)
{
Color c = dimmerImg.color;
dimmerImg.color = new Color(c.r, c.g, c.b, 0f);
}
}
void HideBoxImmediate(RectTransform box)
{
if (box == null)
return;
box.DOKill();
box.localScale = Vector3.zero;
box.gameObject.SetActive(false);
}
void AnimateIn(RectTransform box)
{
box.localScale = Vector3.zero;
box.gameObject.SetActive(true);
showHideSequence = DOTween.Sequence().SetUpdate(true);
if (dimmerImg != null)
showHideSequence.Join(dimmerImg.DOFade(0.8f, ANIMATION_DURATION));
showHideSequence.Join(box.DOScale(1f, ANIMATION_DURATION).SetEase(Ease.OutBack));
}
void AnimateOut(RectTransform box)
{
KillTweens();
RectTransform targetBox = box;
showHideSequence = DOTween.Sequence().SetUpdate(true);
if (dimmerImg != null)
showHideSequence.Join(dimmerImg.DOFade(0f, ANIMATION_DURATION));
showHideSequence.Join(targetBox.DOScale(0f, ANIMATION_DURATION).SetEase(Ease.InBack));
showHideSequence.OnComplete(() =>
{
if (targetBox != null)
targetBox.gameObject.SetActive(false);
if (dimmer != null)
dimmer.gameObject.SetActive(false);
});
}
void KillTweens()
{
if (showHideSequence != null && showHideSequence.IsActive())
showHideSequence.Kill();
showHideSequence = null;
if (dimmerImg != null)
dimmerImg.DOKill();
if (basicMessageBox != null)
basicMessageBox.DOKill();
if (messageBox != null)
messageBox.DOKill();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c50f3cfa5a1c65b4c8b0a011d602717a
@@ -4,6 +4,7 @@ using DG.Tweening;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using System.Collections; using System.Collections;
using TMPro; using TMPro;
using Mirror;
public class LevelLoadManager : MonoBehaviour public class LevelLoadManager : MonoBehaviour
{ {
public static LevelLoadManager instance; public static LevelLoadManager instance;
@@ -87,6 +88,7 @@ public class LevelLoadManager : MonoBehaviour
public void Show(){ public void Show(){
if(isShowing){return;} //already is showing
canvasGroup.blocksRaycasts=true; canvasGroup.blocksRaycasts=true;
canvasGroup.interactable=true; canvasGroup.interactable=true;
contentImage.fillOrigin = 0; contentImage.fillOrigin = 0;
@@ -180,4 +182,26 @@ public class LevelLoadManager : MonoBehaviour
p2_l10.text = $"L10 {opponentPlayer.l10_wins}-{opponentPlayer.l10_losses}"; p2_l10.text = $"L10 {opponentPlayer.l10_wins}-{opponentPlayer.l10_losses}";
} }
public void Leave(string levelName = "MainMenu"){
NetManager.StopNetworkingForSceneChange();
StartCoroutine(CoroutineWaitTillDisconnection(levelName));
}
IEnumerator CoroutineWaitTillDisconnection(string levelName){
Show(); //Load the UI until the level is loaded
const float timeoutSeconds = 5f;
float elapsed = 0f;
while ((NetworkClient.isConnected || NetworkClient.active || NetworkServer.active) && elapsed < timeoutSeconds)
{
elapsed += Time.unscaledDeltaTime;
yield return null;
}
yield return new WaitForSeconds(0.25f);
LoadLevel(levelName);
}
} }
+1
View File
@@ -6,6 +6,7 @@ public class UserData
{ {
public int id; public int id;
public string username; public string username;
public string email;
public int cc; public int cc;
public int rc; public int rc;
public string created_at; public string created_at;
@@ -0,0 +1,10 @@
using System;
using UnityEngine;
[Serializable]
public class PingReportApiResponse
{
public bool ok;
public string error;
public PingReportData report;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 76e4e69960816204d8a01d0e534e0bf4
@@ -0,0 +1,13 @@
using System;
using UnityEngine;
[Serializable]
public class PingReportData
{
public int id;
public int user_id;
public int match_id;
public int ping;
public string ip_address;
public string created_at;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b3f6089d3b18cb34482d7de0cd8bca51
@@ -0,0 +1,14 @@
using System;
[Serializable]
public class PlayerProfileApiResponse
{
public bool ok;
public string error;
public int player_id;
public string username;
public string email;
public int days_since_registration;
public int matches_played;
public float win_ratio;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: acfc3a6cd73c2234081774fcdc39082a
+456
View File
@@ -0,0 +1,456 @@
using System;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
[RequireComponent(typeof(EventTrigger))]
public class CardCarousel : MonoBehaviour
{
[SerializeField] RectTransform[] items;
[SerializeField] float spacing = 40f;
[SerializeField] float swipeThreshold = 60f;
[SerializeField] float snapDuration = 0.25f;
[SerializeField] Ease snapEase = Ease.OutCubic;
[Tooltip("Horizontal half-width of the center tap zone, in local units of this RectTransform.")]
[SerializeField] float centerTapHalfWidth = 260f;
[SerializeField] bool looped = true;
[SerializeField] int selectedIndex;
[Header("Focus Animation")]
[SerializeField] float activeScale = 1.05f;
[SerializeField] float inactiveScale = 0.88f;
[SerializeField] float activePullDown = 35f;
public UnityEvent<int> onSelected = new UnityEvent<int>();
EventTrigger eventTrigger;
RectTransform rectTransform;
RectTransform dragSpace;
RectTransform duplicateClone;
readonly Dictionary<RectTransform, Vector3> baseScales = new();
float slotStep;
float focusX;
float focusY;
float focusPosition;
float dragStartPosition;
float pointerDownLocalX;
bool isDragging;
bool isAnimating;
Tween snapTween;
public int SelectedIndex => selectedIndex;
void Awake()
{
rectTransform = (RectTransform)transform;
eventTrigger = GetComponent<EventTrigger>();
RegisterTrigger(EventTriggerType.PointerDown, OnPointerDown);
RegisterTrigger(EventTriggerType.Drag, OnDrag);
RegisterTrigger(EventTriggerType.PointerUp, OnPointerUp);
}
void Start()
{
if (items == null || items.Length < 2)
{
Debug.LogWarning($"{nameof(CardCarousel)} on {name} needs at least 2 items.", this);
return;
}
selectedIndex = Mathf.Clamp(selectedIndex, 0, items.Length - 1);
dragSpace = items[0].parent as RectTransform;
slotStep = items[0].rect.width + spacing;
focusX = items[0].anchoredPosition.x;
focusY = items[0].anchoredPosition.y;
duplicateClone = CreateClone(items[0], "_carouselClone_Dup");
foreach (var item in items)
CacheBaseScale(item);
focusPosition = selectedIndex;
ApplyLayout();
}
void OnDestroy()
{
snapTween?.Kill();
}
void RegisterTrigger(EventTriggerType type, Action<BaseEventData> handler)
{
var entry = new EventTrigger.Entry { eventID = type };
entry.callback.AddListener(handler.Invoke);
eventTrigger.triggers.Add(entry);
}
RectTransform CreateClone(RectTransform source, string suffix)
{
var clone = Instantiate(source, source.parent);
clone.name = source.name + suffix;
DisableInteractions(clone);
return clone;
}
static void DisableInteractions(RectTransform target)
{
foreach (var graphic in target.GetComponentsInChildren<Graphic>(true))
graphic.raycastTarget = false;
foreach (var button in target.GetComponentsInChildren<Button>(true))
button.interactable = false;
}
void OnPointerDown(BaseEventData eventData)
{
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
return;
snapTween?.Kill();
isAnimating = false;
isDragging = false;
dragStartPosition = focusPosition;
pointerDownLocalX = GetLocalDragX(pointer);
}
void OnDrag(BaseEventData eventData)
{
if (!TryGetPointer(eventData, out var pointer) || items == null || items.Length < 2)
return;
isDragging = true;
float delta = GetLocalDragX(pointer) - pointerDownLocalX;
focusPosition = dragStartPosition - delta / slotStep;
if (!looped)
focusPosition = Mathf.Clamp(focusPosition, 0f, items.Length - 1);
ApplyLayout();
}
void OnPointerUp(BaseEventData eventData)
{
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
return;
float pointerDelta = GetLocalDragX(pointer) - pointerDownLocalX;
float dragDelta = focusPosition - dragStartPosition;
bool passedSwipeThreshold = Mathf.Abs(pointerDelta) >= swipeThreshold
|| Mathf.Abs(dragDelta * slotStep) >= swipeThreshold;
if (!passedSwipeThreshold)
{
if (TryGetTapZone(pointer, out int tapDirection))
{
if (tapDirection == 0)
SelectCurrent();
else if (CanMoveBy(tapDirection))
AnimateToDirection(tapDirection);
else
SnapTo(selectedIndex, null);
}
else
SnapTo(selectedIndex, null);
isDragging = false;
return;
}
isDragging = false;
int direction = dragDelta > 0f ? 1 : -1;
if (Mathf.Abs(focusPosition - selectedIndex) > 0.5f)
{
int nearest = Mathf.RoundToInt(focusPosition);
if (!looped)
nearest = Mathf.Clamp(nearest, 0, items.Length - 1);
if (nearest != selectedIndex && CanMoveBy(nearest > selectedIndex ? 1 : -1))
{
AnimateTo(nearest);
return;
}
}
if (!CanMoveBy(direction))
{
SnapTo(selectedIndex, null);
return;
}
AnimateToDirection(direction);
}
float GetLocalDragX(PointerEventData pointer)
{
if (dragSpace == null)
return pointer.position.x;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
dragSpace, pointer.position, pointer.pressEventCamera, out var local);
return local.x;
}
bool TryGetTapZone(PointerEventData pointer, out int direction)
{
direction = 0;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform, pointer.position, pointer.pressEventCamera, out var local))
return false;
if (Mathf.Abs(local.x) <= centerTapHalfWidth)
return true;
direction = local.x < 0f ? -1 : 1;
return true;
}
void SelectCurrent()
{
if (Mathf.Approximately(focusPosition, selectedIndex))
onSelected.Invoke(selectedIndex);
else
SnapTo(selectedIndex, () => onSelected.Invoke(selectedIndex));
}
void AnimateToDirection(int direction)
{
int newIndex = GetAdjacentIndex(direction, items.Length);
if (newIndex < 0)
return;
AnimateTo(newIndex);
}
void AnimateTo(int newIndex)
{
isAnimating = true;
snapTween?.Kill();
snapTween = DOTween.To(
() => focusPosition,
value =>
{
focusPosition = value;
ApplyLayout();
},
newIndex,
snapDuration)
.SetEase(snapEase)
.OnComplete(() => CompleteSlide(newIndex));
}
void CompleteSlide(int newIndex)
{
selectedIndex = newIndex;
focusPosition = newIndex;
isAnimating = false;
ApplyLayout();
}
void SnapTo(float targetPosition, Action onComplete)
{
isAnimating = true;
snapTween?.Kill();
snapTween = DOTween.To(
() => focusPosition,
value =>
{
focusPosition = value;
ApplyLayout();
},
targetPosition,
snapDuration)
.SetEase(snapEase)
.OnComplete(() =>
{
isAnimating = false;
onComplete?.Invoke();
});
}
float GetItemOffset(int itemIndex)
{
return (itemIndex - focusPosition) * slotStep;
}
bool IsItemVisible(float offset)
{
return Mathf.Abs(offset) <= slotStep * 1.5f;
}
void ApplyLayout()
{
int count = items.Length;
bool useDuplicate = looped && count == 2;
for (int i = 0; i < count; i++)
{
float offset = GetItemOffset(i);
if (IsItemVisible(offset))
{
SetItemTransform(items[i], offset);
items[i].gameObject.SetActive(true);
}
else
items[i].gameObject.SetActive(false);
}
if (useDuplicate && Mathf.Approximately(focusPosition, Mathf.Round(focusPosition)))
{
int restIndex = Mathf.RoundToInt(focusPosition);
int otherIndex = 1 - restIndex;
float otherOffset = GetItemOffset(otherIndex);
float duplicateOffset = -otherOffset;
if (IsItemVisible(duplicateOffset))
{
SyncDuplicateClone(items[otherIndex]);
SetItemTransform(duplicateClone, duplicateOffset);
duplicateClone.gameObject.SetActive(true);
}
else if (duplicateClone != null)
duplicateClone.gameObject.SetActive(false);
}
else if (duplicateClone != null)
{
duplicateClone.gameObject.SetActive(false);
}
int topIndex = 0;
float bestFocus = float.MaxValue;
for (int i = 0; i < count; i++)
{
if (!items[i].gameObject.activeSelf)
continue;
float focus = GetFocusT(GetItemOffset(i));
if (focus < bestFocus)
{
bestFocus = focus;
topIndex = i;
}
}
items[topIndex].SetAsLastSibling();
}
void SyncDuplicateClone(RectTransform source)
{
string expectedName = source.name + "_carouselClone_Dup";
if (duplicateClone == null || duplicateClone.name != expectedName)
{
var parent = source.parent;
int sibling = duplicateClone != null ? duplicateClone.GetSiblingIndex() : source.GetSiblingIndex();
if (duplicateClone != null)
{
baseScales.Remove(duplicateClone);
Destroy(duplicateClone.gameObject);
}
duplicateClone = CreateClone(source, "_carouselClone_Dup");
duplicateClone.SetParent(parent, false);
duplicateClone.SetSiblingIndex(sibling);
}
CacheBaseScale(duplicateClone, source);
}
void CacheBaseScale(RectTransform item, RectTransform source = null)
{
if (baseScales.ContainsKey(item))
return;
baseScales[item] = source != null ? source.localScale : item.localScale;
}
void SetItemTransform(RectTransform item, float xOffset)
{
CacheBaseScale(item);
float focusT = GetFocusT(xOffset);
float scale = Mathf.Lerp(activeScale, inactiveScale, focusT);
item.localScale = baseScales[item] * scale;
var pos = item.anchoredPosition;
pos.x = focusX + xOffset;
pos.y = focusY - Mathf.Lerp(activePullDown, 0f, focusT);
item.anchoredPosition = pos;
}
float GetFocusT(float xOffset)
{
if (slotStep <= 0f)
return 0f;
float t = Mathf.Clamp01(Mathf.Abs(xOffset) / slotStep);
return t * t * (3f - 2f * t);
}
static int WrapIndex(int index, int count)
{
if (count <= 0)
return 0;
return ((index % count) + count) % count;
}
int GetAdjacentIndex(int direction, int count)
{
int index = selectedIndex + direction;
if (looped)
return WrapIndex(index, count);
return index >= 0 && index < count ? index : -1;
}
bool CanMoveBy(int direction)
{
if (items == null || items.Length == 0)
return false;
return GetAdjacentIndex(direction, items.Length) >= 0;
}
int NormalizeIndex(int index)
{
if (items == null || items.Length == 0)
return 0;
return looped ? WrapIndex(index, items.Length) : Mathf.Clamp(index, 0, items.Length - 1);
}
static bool TryGetPointer(BaseEventData eventData, out PointerEventData pointer)
{
pointer = eventData as PointerEventData;
return pointer != null;
}
public void SetSelectedIndex(int index, bool animate = false)
{
if (items == null || items.Length == 0)
return;
int target = NormalizeIndex(index);
if (target == selectedIndex)
return;
if (!animate || !isActiveAndEnabled)
{
snapTween?.Kill();
isAnimating = false;
selectedIndex = target;
focusPosition = target;
ApplyLayout();
return;
}
int direction = target > selectedIndex ? 1 : -1;
if (!CanMoveBy(direction) || Mathf.Abs(target - selectedIndex) != 1)
{
selectedIndex = target;
focusPosition = target;
ApplyLayout();
return;
}
AnimateToDirection(direction);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a4951ec6b41c5d14cb8f5a8803dfa0c2
+17 -15
View File
@@ -3,9 +3,9 @@ using UnityEngine.Rendering;
using UnityEngine.UI; using UnityEngine.UI;
/// <summary> /// <summary>
/// Inverted stencil mask (see materialForRendering). For raycasts, assign /// Inverted stencil mask (see materialForRendering). Raycast options live on
/// <see cref="raycastPassThroughRects"/> to rects that match each visual "hole" /// <see cref="CutoutMaskUISettings"/> on the same GameObject (Unity's Image
/// — clicks there wont hit this graphic and pass to UI behind. /// inspector does not show fields on Image subclasses).
/// ///
/// Parent <see cref="Mask"/> also implements <see cref="ICanvasRaycastFilter"/> and only /// Parent <see cref="Mask"/> also implements <see cref="ICanvasRaycastFilter"/> and only
/// allows points inside its RectTransform. A fullscreen child under a small mask would /// allows points inside its RectTransform. A fullscreen child under a small mask would
@@ -15,13 +15,10 @@ using UnityEngine.UI;
/// ignoreMasks) so hit testing matches the fullscreen Image; rendering still uses stencil /// ignoreMasks) so hit testing matches the fullscreen Image; rendering still uses stencil
/// because <see cref="MaskableGraphic.maskable"/> stays true. /// because <see cref="MaskableGraphic.maskable"/> stays true.
/// </summary> /// </summary>
[RequireComponent(typeof(CutoutMaskUISettings))]
public class CutoutMaskUI : Image public class CutoutMaskUI : Image
{ {
[Tooltip("Rects matching cutout/hole areas. Clicks inside are not consumed by this Image. Leave empty to use the parent Masks RectTransform.")] CutoutMaskUISettings settings;
[SerializeField] RectTransform[] raycastPassThroughRects;
[Tooltip("The Image on the same object as Mask is usually the cutout shape and still receives raycasts. When this is on, that graphics Raycast Target is turned off while this component is enabled so the hole reaches UI behind the overlay.")]
[SerializeField] bool disableRaycastOnParentMaskGraphic = true;
Mask cachedParentMask; Mask cachedParentMask;
Graphic cachedMaskGraphic; Graphic cachedMaskGraphic;
@@ -33,8 +30,10 @@ public class CutoutMaskUI : Image
return Raycast(sp, eventCamera, ignoreMasks: true); return Raycast(sp, eventCamera, ignoreMasks: true);
} }
public override Material materialForRendering{ public override Material materialForRendering
get{ {
get
{
Material material = new Material(base.materialForRendering); Material material = new Material(base.materialForRendering);
material.SetInt("_StencilComp", (int)CompareFunction.NotEqual); material.SetInt("_StencilComp", (int)CompareFunction.NotEqual);
return material; return material;
@@ -43,8 +42,10 @@ public class CutoutMaskUI : Image
protected override void OnEnable() protected override void OnEnable()
{ {
settings = GetComponent<CutoutMaskUISettings>();
base.OnEnable(); base.OnEnable();
if (!disableRaycastOnParentMaskGraphic)
if (settings == null || !settings.disableRaycastOnParentMaskGraphic)
return; return;
cachedParentMask = GetComponentInParent<Mask>(); cachedParentMask = GetComponentInParent<Mask>();
@@ -76,18 +77,19 @@ public class CutoutMaskUI : Image
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{ {
if (IsInsidePassThroughHole(screenPoint, eventCamera)) if (IsInsidePassThroughHole(screenPoint, eventCamera))
return false; return settings != null && settings.blockRaycastThroughCutout;
return base.IsRaycastLocationValid(screenPoint, eventCamera); return base.IsRaycastLocationValid(screenPoint, eventCamera);
} }
bool IsInsidePassThroughHole(Vector2 screenPoint, Camera eventCamera) bool IsInsidePassThroughHole(Vector2 screenPoint, Camera eventCamera)
{ {
if (raycastPassThroughRects != null) RectTransform[] passThroughRects = settings != null ? settings.raycastPassThroughRects : null;
if (passThroughRects != null)
{ {
for (int i = 0; i < raycastPassThroughRects.Length; i++) for (int i = 0; i < passThroughRects.Length; i++)
{ {
RectTransform rect = raycastPassThroughRects[i]; RectTransform rect = passThroughRects[i];
if (rect != null && RectTransformUtility.RectangleContainsScreenPoint(rect, screenPoint, eventCamera)) if (rect != null && RectTransformUtility.RectangleContainsScreenPoint(rect, screenPoint, eventCamera))
return true; return true;
} }
@@ -0,0 +1,15 @@
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(CutoutMaskUI))]
public class CutoutMaskUISettings : MonoBehaviour
{
[Tooltip("Rects matching cutout/hole areas. Clicks inside are not consumed by this Image. Leave empty to use the parent Mask's RectTransform.")]
public RectTransform[] raycastPassThroughRects;
[Tooltip("The Image on the same object as Mask is usually the cutout shape and still receives raycasts. When this is on, that graphic's Raycast Target is turned off while this component is enabled so the hole reaches UI behind the overlay.")]
public bool disableRaycastOnParentMaskGraphic = true;
[Tooltip("When enabled, clicks inside cutout/hole areas are consumed by this Image instead of passing through to UI behind.")]
public bool blockRaycastThroughCutout;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 29b95265513fc314995c59cb90745214
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 05aa9e67ef3520047a8c20a6f96165d8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+63 -12
View File
@@ -19,6 +19,7 @@ public class FrameAnimator : MonoBehaviour
bool m_previewPlaying = false; bool m_previewPlaying = false;
#if UNITY_EDITOR #if UNITY_EDITOR
double m_lastEditorTime = 0d; double m_lastEditorTime = 0d;
bool m_editorHooked;
#endif #endif
Image m_img; Image m_img;
Image img { Image img {
@@ -42,19 +43,19 @@ public class FrameAnimator : MonoBehaviour
void OnEnable() void OnEnable()
{ {
#if UNITY_EDITOR #if UNITY_EDITOR
if (!Application.isPlaying) // Never hook editor updates during play-mode enter/exit — QueuePlayerLoopUpdate
{ // + isPlaying serialized true can infinite-reload Temp/__Backupscenes.
m_lastEditorTime = EditorApplication.timeSinceStartup; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorApplication.update -= OnEditorUpdate; EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
EditorApplication.update += OnEditorUpdate; TryHookEditorUpdate();
}
#endif #endif
} }
void OnDisable() void OnDisable()
{ {
#if UNITY_EDITOR #if UNITY_EDITOR
EditorApplication.update -= OnEditorUpdate; UnhookEditorUpdate();
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
#endif #endif
} }
void Start() void Start()
@@ -80,23 +81,71 @@ public class FrameAnimator : MonoBehaviour
} }
#if UNITY_EDITOR #if UNITY_EDITOR
void OnEditorUpdate() void OnPlayModeStateChanged(PlayModeStateChange state)
{ {
if (Application.isPlaying || this == null || !isActiveAndEnabled) if (state == PlayModeStateChange.ExitingEditMode ||
state == PlayModeStateChange.EnteredPlayMode ||
state == PlayModeStateChange.ExitingPlayMode)
{ {
m_previewPlaying = false;
UnhookEditorUpdate();
}
else if (state == PlayModeStateChange.EnteredEditMode)
{
TryHookEditorUpdate();
}
}
void TryHookEditorUpdate()
{
if (Application.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode)
{
UnhookEditorUpdate();
return; return;
} }
if (m_editorHooked)
return;
m_lastEditorTime = EditorApplication.timeSinceStartup;
EditorApplication.update += OnEditorUpdate;
m_editorHooked = true;
}
void UnhookEditorUpdate()
{
if (!m_editorHooked)
return;
EditorApplication.update -= OnEditorUpdate;
m_editorHooked = false;
}
void OnEditorUpdate()
{
if (Application.isPlaying ||
EditorApplication.isPlayingOrWillChangePlaymode ||
this == null ||
!isActiveAndEnabled)
{
UnhookEditorUpdate();
return;
}
// Edit-mode animation is preview-only. Ignoring serialized isPlaying prevents
// permanent editor loops when isPlaying was left true in the scene.
if (!m_previewPlaying)
return;
double now = EditorApplication.timeSinceStartup; double now = EditorApplication.timeSinceStartup;
float dt = (float)(now - m_lastEditorTime); float dt = (float)(now - m_lastEditorTime);
m_lastEditorTime = now; m_lastEditorTime = now;
if (dt <= 0f) if (dt <= 0f)
{
return; return;
}
int frameBefore = currentFrame;
TickAnimation(dt); TickAnimation(dt);
if (currentFrame != frameBefore || m_previewPlaying)
EditorApplication.QueuePlayerLoopUpdate(); EditorApplication.QueuePlayerLoopUpdate();
} }
#endif #endif
@@ -108,7 +157,9 @@ public class FrameAnimator : MonoBehaviour
return; return;
} }
bool shouldAnimate = m_previewPlaying || isPlaying; bool shouldAnimate = Application.isPlaying
? (m_previewPlaying || isPlaying)
: m_previewPlaying;
if (!shouldAnimate) if (!shouldAnimate)
{ {
return; return;
@@ -0,0 +1,58 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
m_Name: Android
m_EditorClassIdentifier:
m_AssetVersion: 1
m_BuildTarget: 13
m_Subtarget: 0
m_PlatformId: b9b35072a6f44c2e863f17467ea3dc13
m_PlatformBuildProfile:
rid: 1400252592429989960
m_OverrideGlobalSceneList: 1
m_Scenes:
- m_enabled: 1
m_path: Assets/Scenes/Intro.unity
- m_enabled: 1
m_path: Assets/Scenes/MainMenu.unity
- m_enabled: 1
m_path: Assets/Scenes/Game.unity
m_ScriptingDefines: []
m_PlayerSettingsYaml:
m_Settings: []
references:
version: 2
RefIds:
- rid: 1400252592429989960
type: {class: AndroidPlatformBuildSettings, ns: UnityEditor.Android, asm: UnityEditor.Android.Extensions}
data:
m_Development: 0
m_ConnectProfiler: 0
m_BuildWithDeepProfilingSupport: 0
m_AllowDebugging: 0
m_WaitForManagedDebugger: 0
m_ManagedDebuggerFixedPort: 0
m_ExplicitNullChecks: 0
m_ExplicitDivideByZeroChecks: 0
m_ExplicitArrayBoundsChecks: 0
m_CompressionType: 2
m_InstallInBuildFolder: 0
m_BuildSubtarget: 0
m_BuildSystem: 1
m_ExportAsGoogleAndroidProject: 0
m_DebugSymbolLevel: 1
m_DebugSymbolFormat: 5
m_CurrentDeploymentTargetId: __builtin__target_default
m_BuildType: 2
m_BuildAppBundle: 0
m_IPAddressToConnect:
m_SymlinkSources: 0
@@ -1,863 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
m_Name: "New Android\u2122 Profile"
m_EditorClassIdentifier:
m_AssetVersion: 1
m_BuildTarget: 13
m_Subtarget: 0
m_PlatformId: b9b35072a6f44c2e863f17467ea3dc13
m_PlatformBuildProfile:
rid: 1400252592429989960
m_OverrideGlobalSceneList: 1
m_Scenes:
- m_enabled: 1
m_path: Assets/Scenes/MainMenu.unity
- m_enabled: 1
m_path: Assets/Scenes/Proto.unity
m_ScriptingDefines: []
m_PlayerSettingsYaml:
m_Settings:
- line: '| PlayerSettings:'
- line: '| m_ObjectHideFlags: 0'
- line: '| serializedVersion: 28'
- line: '| productGUID: 73659953e96de4447bb9eeb64c3da355'
- line: '| AndroidProfiler: 0'
- line: '| AndroidFilterTouchesWhenObscured: 0'
- line: '| AndroidEnableSustainedPerformanceMode: 0'
- line: '| defaultScreenOrientation: 4'
- line: '| targetDevice: 2'
- line: '| useOnDemandResources: 0'
- line: '| accelerometerFrequency: 60'
- line: '| companyName: Xperience'
- line: '| productName: WalkInvest_Soccer'
- line: '| defaultCursor: {instanceID: 0}'
- line: '| cursorHotspot: {x: 0, y: 0}'
- line: '| m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b:
0.1254902, a: 1}'
- line: '| m_ShowUnitySplashScreen: 1'
- line: '| m_ShowUnitySplashLogo: 1'
- line: '| m_SplashScreenOverlayOpacity: 1'
- line: '| m_SplashScreenAnimation: 1'
- line: '| m_SplashScreenLogoStyle: 1'
- line: '| m_SplashScreenDrawMode: 0'
- line: '| m_SplashScreenBackgroundAnimationZoom: 1'
- line: '| m_SplashScreenLogoAnimationZoom: 1'
- line: '| m_SplashScreenBackgroundLandscapeAspect: 1'
- line: '| m_SplashScreenBackgroundPortraitAspect: 1'
- line: '| m_SplashScreenBackgroundLandscapeUvs:'
- line: '| serializedVersion: 2'
- line: '| x: 0'
- line: '| y: 0'
- line: '| width: 1'
- line: '| height: 1'
- line: '| m_SplashScreenBackgroundPortraitUvs:'
- line: '| serializedVersion: 2'
- line: '| x: 0'
- line: '| y: 0'
- line: '| width: 1'
- line: '| height: 1'
- line: '| m_SplashScreenLogos: []'
- line: '| m_VirtualRealitySplashScreen: {instanceID: 0}'
- line: '| m_HolographicTrackingLossScreen: {instanceID: 0}'
- line: '| defaultScreenWidth: 1920'
- line: '| defaultScreenHeight: 1080'
- line: '| defaultScreenWidthWeb: 960'
- line: '| defaultScreenHeightWeb: 600'
- line: '| m_StereoRenderingPath: 0'
- line: '| m_ActiveColorSpace: 1'
- line: '| unsupportedMSAAFallback: 0'
- line: '| m_SpriteBatchMaxVertexCount: 65535'
- line: '| m_SpriteBatchVertexThreshold: 300'
- line: '| m_MTRendering: 1'
- line: '| mipStripping: 0'
- line: '| numberOfMipsStripped: 0'
- line: '| numberOfMipsStrippedPerMipmapLimitGroup: {}'
- line: '| m_StackTraceTypes: 010000000100000001000000010000000100000001000000'
- line: '| iosShowActivityIndicatorOnLoading: -1'
- line: '| androidShowActivityIndicatorOnLoading: -1'
- line: '| iosUseCustomAppBackgroundBehavior: 0'
- line: '| allowedAutorotateToPortrait: 1'
- line: '| allowedAutorotateToPortraitUpsideDown: 1'
- line: '| allowedAutorotateToLandscapeRight: 0'
- line: '| allowedAutorotateToLandscapeLeft: 0'
- line: '| useOSAutorotation: 1'
- line: '| use32BitDisplayBuffer: 1'
- line: '| preserveFramebufferAlpha: 0'
- line: '| disableDepthAndStencilBuffers: 0'
- line: '| androidStartInFullscreen: 1'
- line: '| androidRenderOutsideSafeArea: 1'
- line: '| androidUseSwappy: 1'
- line: '| androidBlitType: 0'
- line: '| androidResizeableActivity: 1'
- line: '| androidDefaultWindowWidth: 1920'
- line: '| androidDefaultWindowHeight: 1080'
- line: '| androidMinimumWindowWidth: 400'
- line: '| androidMinimumWindowHeight: 300'
- line: '| androidFullscreenMode: 1'
- line: '| androidAutoRotationBehavior: 1'
- line: '| androidPredictiveBackSupport: 0'
- line: '| androidApplicationEntry: 2'
- line: '| defaultIsNativeResolution: 1'
- line: '| macRetinaSupport: 1'
- line: '| runInBackground: 0'
- line: '| muteOtherAudioSources: 0'
- line: '| Prepare IOS For Recording: 0'
- line: '| Force IOS Speakers When Recording: 0'
- line: '| audioSpatialExperience: 0'
- line: '| deferSystemGesturesMode: 0'
- line: '| hideHomeButton: 0'
- line: '| submitAnalytics: 1'
- line: '| usePlayerLog: 1'
- line: '| dedicatedServerOptimizations: 1'
- line: '| bakeCollisionMeshes: 0'
- line: '| forceSingleInstance: 0'
- line: '| useFlipModelSwapchain: 1'
- line: '| resizableWindow: 0'
- line: '| useMacAppStoreValidation: 0'
- line: '| macAppStoreCategory: public.app-category.games'
- line: '| gpuSkinning: 0'
- line: '| meshDeformation: 0'
- line: '| xboxPIXTextureCapture: 0'
- line: '| xboxEnableAvatar: 0'
- line: '| xboxEnableKinect: 0'
- line: '| xboxEnableKinectAutoTracking: 0'
- line: '| xboxEnableFitness: 0'
- line: '| visibleInBackground: 1'
- line: '| allowFullscreenSwitch: 1'
- line: '| fullscreenMode: 1'
- line: '| xboxSpeechDB: 0'
- line: '| xboxEnableHeadOrientation: 0'
- line: '| xboxEnableGuest: 0'
- line: '| xboxEnablePIXSampling: 0'
- line: '| metalFramebufferOnly: 0'
- line: '| xboxOneResolution: 0'
- line: '| xboxOneSResolution: 0'
- line: '| xboxOneXResolution: 3'
- line: '| xboxOneMonoLoggingLevel: 0'
- line: '| xboxOneLoggingLevel: 1'
- line: '| xboxOneDisableEsram: 0'
- line: '| xboxOneEnableTypeOptimization: 0'
- line: '| xboxOnePresentImmediateThreshold: 0'
- line: '| switchQueueCommandMemory: 1048576'
- line: '| switchQueueControlMemory: 16384'
- line: '| switchQueueComputeMemory: 262144'
- line: '| switchNVNShaderPoolsGranularity: 33554432'
- line: '| switchNVNDefaultPoolsGranularity: 16777216'
- line: '| switchNVNOtherPoolsGranularity: 16777216'
- line: '| switchGpuScratchPoolGranularity: 2097152'
- line: '| switchAllowGpuScratchShrinking: 0'
- line: '| switchNVNMaxPublicTextureIDCount: 0'
- line: '| switchNVNMaxPublicSamplerIDCount: 0'
- line: '| switchMaxWorkerMultiple: 8'
- line: '| switchNVNGraphicsFirmwareMemory: 32'
- line: '| vulkanNumSwapchainBuffers: 3'
- line: '| vulkanEnableSetSRGBWrite: 0'
- line: '| vulkanEnablePreTransform: 0'
- line: '| vulkanEnableLateAcquireNextImage: 0'
- line: '| vulkanEnableCommandBufferRecycling: 1'
- line: '| loadStoreDebugModeEnabled: 0'
- line: '| visionOSBundleVersion: 1.0'
- line: '| tvOSBundleVersion: 1.0'
- line: '| bundleVersion: 0.4'
- line: '| preloadedAssets: []'
- line: '| metroInputSource: 0'
- line: '| wsaTransparentSwapchain: 0'
- line: '| m_HolographicPauseOnTrackingLoss: 1'
- line: '| xboxOneDisableKinectGpuReservation: 1'
- line: '| xboxOneEnable7thCore: 1'
- line: '| vrSettings:'
- line: '| enable360StereoCapture: 0'
- line: '| isWsaHolographicRemotingEnabled: 0'
- line: '| enableFrameTimingStats: 0'
- line: '| enableOpenGLProfilerGPURecorders: 1'
- line: '| allowHDRDisplaySupport: 0'
- line: '| useHDRDisplay: 0'
- line: '| hdrBitDepth: 0'
- line: '| m_ColorGamuts: 00000000'
- line: '| targetPixelDensity: 30'
- line: '| resolutionScalingMode: 0'
- line: '| resetResolutionOnWindowResize: 0'
- line: '| androidSupportedAspectRatio: 1'
- line: '| androidMaxAspectRatio: 2.4'
- line: '| androidMinAspectRatio: 1'
- line: '| applicationIdentifier:'
- line: '| Standalone: com.DefaultCompany.2D-URP'
- line: '| buildNumber:'
- line: '| Standalone: 0'
- line: '| VisionOS: 0'
- line: '| iPhone: 0'
- line: '| tvOS: 0'
- line: '| overrideDefaultApplicationIdentifier: 1'
- line: '| AndroidBundleVersionCode: 1'
- line: '| AndroidMinSdkVersion: 23'
- line: '| AndroidTargetSdkVersion: 0'
- line: '| AndroidPreferredInstallLocation: 1'
- line: '| aotOptions: '
- line: '| stripEngineCode: 1'
- line: '| iPhoneStrippingLevel: 0'
- line: '| iPhoneScriptCallOptimization: 0'
- line: '| ForceInternetPermission: 0'
- line: '| ForceSDCardPermission: 0'
- line: '| CreateWallpaper: 0'
- line: '| androidSplitApplicationBinary: 0'
- line: '| keepLoadedShadersAlive: 0'
- line: '| StripUnusedMeshComponents: 0'
- line: '| strictShaderVariantMatching: 0'
- line: '| VertexChannelCompressionMask: 4054'
- line: '| iPhoneSdkVersion: 988'
- line: '| iOSSimulatorArchitecture: 0'
- line: '| iOSTargetOSVersionString: 13.0'
- line: '| tvOSSdkVersion: 0'
- line: '| tvOSSimulatorArchitecture: 0'
- line: '| tvOSRequireExtendedGameController: 0'
- line: '| tvOSTargetOSVersionString: 13.0'
- line: '| VisionOSSdkVersion: 0'
- line: '| VisionOSTargetOSVersionString: 1.0'
- line: '| uIPrerenderedIcon: 0'
- line: '| uIRequiresPersistentWiFi: 0'
- line: '| uIRequiresFullScreen: 1'
- line: '| uIStatusBarHidden: 1'
- line: '| uIExitOnSuspend: 0'
- line: '| uIStatusBarStyle: 0'
- line: '| appleTVSplashScreen: {instanceID: 0}'
- line: '| appleTVSplashScreen2x: {instanceID: 0}'
- line: '| tvOSSmallIconLayers: []'
- line: '| tvOSSmallIconLayers2x: []'
- line: '| tvOSLargeIconLayers: []'
- line: '| tvOSLargeIconLayers2x: []'
- line: '| tvOSTopShelfImageLayers: []'
- line: '| tvOSTopShelfImageLayers2x: []'
- line: '| tvOSTopShelfImageWideLayers: []'
- line: '| tvOSTopShelfImageWideLayers2x: []'
- line: '| iOSLaunchScreenType: 0'
- line: '| iOSLaunchScreenPortrait: {instanceID: 0}'
- line: '| iOSLaunchScreenLandscape: {instanceID: 0}'
- line: '| iOSLaunchScreenBackgroundColor:'
- line: '| serializedVersion: 2'
- line: '| rgba: 0'
- line: '| iOSLaunchScreenFillPct: 100'
- line: '| iOSLaunchScreenSize: 100'
- line: '| iOSLaunchScreeniPadType: 0'
- line: '| iOSLaunchScreeniPadImage: {instanceID: 0}'
- line: '| iOSLaunchScreeniPadBackgroundColor:'
- line: '| serializedVersion: 2'
- line: '| rgba: 0'
- line: '| iOSLaunchScreeniPadFillPct: 100'
- line: '| iOSLaunchScreeniPadSize: 100'
- line: '| iOSLaunchScreenCustomStoryboardPath: '
- line: '| iOSLaunchScreeniPadCustomStoryboardPath: '
- line: '| iOSDeviceRequirements: []'
- line: '| iOSURLSchemes: []'
- line: '| macOSURLSchemes: []'
- line: '| iOSBackgroundModes: 0'
- line: '| iOSMetalForceHardShadows: 0'
- line: '| metalEditorSupport: 1'
- line: '| metalAPIValidation: 1'
- line: '| metalCompileShaderBinary: 0'
- line: '| iOSRenderExtraFrameOnPause: 0'
- line: '| iosCopyPluginsCodeInsteadOfSymlink: 0'
- line: '| appleDeveloperTeamID: '
- line: '| iOSManualSigningProvisioningProfileID: '
- line: '| tvOSManualSigningProvisioningProfileID: '
- line: '| VisionOSManualSigningProvisioningProfileID: '
- line: '| iOSManualSigningProvisioningProfileType: 0'
- line: '| tvOSManualSigningProvisioningProfileType: 0'
- line: '| VisionOSManualSigningProvisioningProfileType: 0'
- line: '| appleEnableAutomaticSigning: 0'
- line: '| iOSRequireARKit: 0'
- line: '| iOSAutomaticallyDetectAndAddCapabilities: 1'
- line: '| appleEnableProMotion: 0'
- line: '| shaderPrecisionModel: 0'
- line: '| clonedFromGUID: c19f32bac17ee4170b3bf8a6a0333fb9'
- line: '| templatePackageId: com.unity.template.universal-2d@5.1.0'
- line: '| templateDefaultScene: Assets/Scenes/SampleScene.unity'
- line: '| useCustomMainManifest: 0'
- line: '| useCustomLauncherManifest: 0'
- line: '| useCustomMainGradleTemplate: 0'
- line: '| useCustomLauncherGradleManifest: 0'
- line: '| useCustomBaseGradleTemplate: 0'
- line: '| useCustomGradlePropertiesTemplate: 0'
- line: '| useCustomGradleSettingsTemplate: 0'
- line: '| useCustomProguardFile: 0'
- line: '| AndroidTargetArchitectures: 2'
- line: '| AndroidSplashScreenScale: 0'
- line: '| androidSplashScreen: {instanceID: 0}'
- line: '| AndroidKeystoreName: '
- line: '| AndroidKeyaliasName: '
- line: '| AndroidEnableArmv9SecurityFeatures: 0'
- line: '| AndroidEnableArm64MTE: 0'
- line: '| AndroidBuildApkPerCpuArchitecture: 0'
- line: '| AndroidTVCompatibility: 0'
- line: '| AndroidIsGame: 1'
- line: '| androidAppCategory: 3'
- line: '| useAndroidAppCategory: 1'
- line: '| androidAppCategoryOther: '
- line: '| AndroidEnableTango: 0'
- line: '| androidEnableBanner: 1'
- line: '| androidUseLowAccuracyLocation: 0'
- line: '| androidUseCustomKeystore: 0'
- line: '| m_AndroidBanners:'
- line: '| - width: 320'
- line: '| height: 180'
- line: '| banner: {instanceID: 0}'
- line: '| androidGamepadSupportLevel: 0'
- line: '| AndroidMinifyRelease: 0'
- line: '| AndroidMinifyDebug: 0'
- line: '| AndroidValidateAppBundleSize: 1'
- line: '| AndroidAppBundleSizeToValidate: 150'
- line: '| AndroidReportGooglePlayAppDependencies: 1'
- line: '| androidSymbolsSizeThreshold: 800'
- line: '| m_BuildTargetIcons: []'
- line: '| m_BuildTargetPlatformIcons:'
- line: '| - m_BuildTarget: Android'
- line: '| m_Icons:'
- line: '| - m_Textures: []'
- line: '| m_Width: 432'
- line: '| m_Height: 432'
- line: '| m_Kind: 2'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 324'
- line: '| m_Height: 324'
- line: '| m_Kind: 2'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 216'
- line: '| m_Height: 216'
- line: '| m_Kind: 2'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 162'
- line: '| m_Height: 162'
- line: '| m_Kind: 2'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 108'
- line: '| m_Height: 108'
- line: '| m_Kind: 2'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 81'
- line: '| m_Height: 81'
- line: '| m_Kind: 2'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 192'
- line: '| m_Height: 192'
- line: '| m_Kind: 1'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 144'
- line: '| m_Height: 144'
- line: '| m_Kind: 1'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 96'
- line: '| m_Height: 96'
- line: '| m_Kind: 1'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 72'
- line: '| m_Height: 72'
- line: '| m_Kind: 1'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 48'
- line: '| m_Height: 48'
- line: '| m_Kind: 1'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 36'
- line: '| m_Height: 36'
- line: '| m_Kind: 1'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 192'
- line: '| m_Height: 192'
- line: '| m_Kind: 0'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 144'
- line: '| m_Height: 144'
- line: '| m_Kind: 0'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 96'
- line: '| m_Height: 96'
- line: '| m_Kind: 0'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 72'
- line: '| m_Height: 72'
- line: '| m_Kind: 0'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 48'
- line: '| m_Height: 48'
- line: '| m_Kind: 0'
- line: '| m_SubKind: '
- line: '| - m_Textures: []'
- line: '| m_Width: 36'
- line: '| m_Height: 36'
- line: '| m_Kind: 0'
- line: '| m_SubKind: '
- line: '| m_BuildTargetBatching: []'
- line: '| m_BuildTargetShaderSettings: []'
- line: '| m_BuildTargetGraphicsJobs: []'
- line: '| m_BuildTargetGraphicsJobMode: []'
- line: '| m_BuildTargetGraphicsAPIs: []'
- line: '| m_BuildTargetVRSettings: []'
- line: '| m_DefaultShaderChunkSizeInMB: 16'
- line: '| m_DefaultShaderChunkCount: 0'
- line: '| openGLRequireES31: 0'
- line: '| openGLRequireES31AEP: 0'
- line: '| openGLRequireES32: 0'
- line: '| m_TemplateCustomTags: {}'
- line: '| mobileMTRendering:'
- line: '| Android: 1'
- line: '| iPhone: 1'
- line: '| tvOS: 1'
- line: '| m_BuildTargetGroupLightmapEncodingQuality: []'
- line: '| m_BuildTargetGroupHDRCubemapEncodingQuality: []'
- line: '| m_BuildTargetGroupLightmapSettings: []'
- line: '| m_BuildTargetGroupLoadStoreDebugModeSettings: []'
- line: '| m_BuildTargetNormalMapEncoding: []'
- line: '| m_BuildTargetDefaultTextureCompressionFormat:'
- line: '| - serializedVersion: 3'
- line: '| m_BuildTarget: Android'
- line: '| m_Formats: 01000000'
- line: '| playModeTestRunnerEnabled: 0'
- line: '| runPlayModeTestAsEditModeTest: 0'
- line: '| actionOnDotNetUnhandledException: 1'
- line: '| editorGfxJobOverride: 1'
- line: '| enableInternalProfiler: 0'
- line: '| logObjCUncaughtExceptions: 1'
- line: '| enableCrashReportAPI: 0'
- line: '| cameraUsageDescription: '
- line: '| locationUsageDescription: '
- line: '| microphoneUsageDescription: '
- line: '| bluetoothUsageDescription: '
- line: '| macOSTargetOSVersion: 11.0'
- line: '| switchNMETAOverride: '
- line: '| switchNetLibKey: '
- line: '| switchSocketMemoryPoolSize: 6144'
- line: '| switchSocketAllocatorPoolSize: 128'
- line: '| switchSocketConcurrencyLimit: 14'
- line: '| switchScreenResolutionBehavior: 2'
- line: '| switchUseCPUProfiler: 0'
- line: '| switchEnableFileSystemTrace: 0'
- line: '| switchLTOSetting: 0'
- line: '| switchApplicationID: 0x01004b9000490000'
- line: '| switchNSODependencies: '
- line: '| switchCompilerFlags: '
- line: '| switchTitleNames_0: '
- line: '| switchTitleNames_1: '
- line: '| switchTitleNames_2: '
- line: '| switchTitleNames_3: '
- line: '| switchTitleNames_4: '
- line: '| switchTitleNames_5: '
- line: '| switchTitleNames_6: '
- line: '| switchTitleNames_7: '
- line: '| switchTitleNames_8: '
- line: '| switchTitleNames_9: '
- line: '| switchTitleNames_10: '
- line: '| switchTitleNames_11: '
- line: '| switchTitleNames_12: '
- line: '| switchTitleNames_13: '
- line: '| switchTitleNames_14: '
- line: '| switchTitleNames_15: '
- line: '| switchPublisherNames_0: '
- line: '| switchPublisherNames_1: '
- line: '| switchPublisherNames_2: '
- line: '| switchPublisherNames_3: '
- line: '| switchPublisherNames_4: '
- line: '| switchPublisherNames_5: '
- line: '| switchPublisherNames_6: '
- line: '| switchPublisherNames_7: '
- line: '| switchPublisherNames_8: '
- line: '| switchPublisherNames_9: '
- line: '| switchPublisherNames_10: '
- line: '| switchPublisherNames_11: '
- line: '| switchPublisherNames_12: '
- line: '| switchPublisherNames_13: '
- line: '| switchPublisherNames_14: '
- line: '| switchPublisherNames_15: '
- line: '| switchIcons_0: {instanceID: 0}'
- line: '| switchIcons_1: {instanceID: 0}'
- line: '| switchIcons_2: {instanceID: 0}'
- line: '| switchIcons_3: {instanceID: 0}'
- line: '| switchIcons_4: {instanceID: 0}'
- line: '| switchIcons_5: {instanceID: 0}'
- line: '| switchIcons_6: {instanceID: 0}'
- line: '| switchIcons_7: {instanceID: 0}'
- line: '| switchIcons_8: {instanceID: 0}'
- line: '| switchIcons_9: {instanceID: 0}'
- line: '| switchIcons_10: {instanceID: 0}'
- line: '| switchIcons_11: {instanceID: 0}'
- line: '| switchIcons_12: {instanceID: 0}'
- line: '| switchIcons_13: {instanceID: 0}'
- line: '| switchIcons_14: {instanceID: 0}'
- line: '| switchIcons_15: {instanceID: 0}'
- line: '| switchSmallIcons_0: {instanceID: 0}'
- line: '| switchSmallIcons_1: {instanceID: 0}'
- line: '| switchSmallIcons_2: {instanceID: 0}'
- line: '| switchSmallIcons_3: {instanceID: 0}'
- line: '| switchSmallIcons_4: {instanceID: 0}'
- line: '| switchSmallIcons_5: {instanceID: 0}'
- line: '| switchSmallIcons_6: {instanceID: 0}'
- line: '| switchSmallIcons_7: {instanceID: 0}'
- line: '| switchSmallIcons_8: {instanceID: 0}'
- line: '| switchSmallIcons_9: {instanceID: 0}'
- line: '| switchSmallIcons_10: {instanceID: 0}'
- line: '| switchSmallIcons_11: {instanceID: 0}'
- line: '| switchSmallIcons_12: {instanceID: 0}'
- line: '| switchSmallIcons_13: {instanceID: 0}'
- line: '| switchSmallIcons_14: {instanceID: 0}'
- line: '| switchSmallIcons_15: {instanceID: 0}'
- line: '| switchManualHTML: '
- line: '| switchAccessibleURLs: '
- line: '| switchLegalInformation: '
- line: '| switchMainThreadStackSize: 1048576'
- line: '| switchPresenceGroupId: '
- line: '| switchLogoHandling: 0'
- line: '| switchReleaseVersion: 0'
- line: '| switchDisplayVersion: 1.0.0'
- line: '| switchStartupUserAccount: 0'
- line: '| switchSupportedLanguagesMask: 0'
- line: '| switchLogoType: 0'
- line: '| switchApplicationErrorCodeCategory: '
- line: '| switchUserAccountSaveDataSize: 0'
- line: '| switchUserAccountSaveDataJournalSize: 0'
- line: '| switchApplicationAttribute: 0'
- line: '| switchCardSpecSize: -1'
- line: '| switchCardSpecClock: -1'
- line: '| switchRatingsMask: 0'
- line: '| switchRatingsInt_0: 0'
- line: '| switchRatingsInt_1: 0'
- line: '| switchRatingsInt_2: 0'
- line: '| switchRatingsInt_3: 0'
- line: '| switchRatingsInt_4: 0'
- line: '| switchRatingsInt_5: 0'
- line: '| switchRatingsInt_6: 0'
- line: '| switchRatingsInt_7: 0'
- line: '| switchRatingsInt_8: 0'
- line: '| switchRatingsInt_9: 0'
- line: '| switchRatingsInt_10: 0'
- line: '| switchRatingsInt_11: 0'
- line: '| switchRatingsInt_12: 0'
- line: '| switchLocalCommunicationIds_0: '
- line: '| switchLocalCommunicationIds_1: '
- line: '| switchLocalCommunicationIds_2: '
- line: '| switchLocalCommunicationIds_3: '
- line: '| switchLocalCommunicationIds_4: '
- line: '| switchLocalCommunicationIds_5: '
- line: '| switchLocalCommunicationIds_6: '
- line: '| switchLocalCommunicationIds_7: '
- line: '| switchParentalControl: 0'
- line: '| switchAllowsScreenshot: 1'
- line: '| switchAllowsVideoCapturing: 1'
- line: '| switchAllowsRuntimeAddOnContentInstall: 0'
- line: '| switchDataLossConfirmation: 0'
- line: '| switchUserAccountLockEnabled: 0'
- line: '| switchSystemResourceMemory: 16777216'
- line: '| switchSupportedNpadStyles: 22'
- line: '| switchNativeFsCacheSize: 32'
- line: '| switchIsHoldTypeHorizontal: 0'
- line: '| switchSupportedNpadCount: 8'
- line: '| switchEnableTouchScreen: 1'
- line: '| switchSocketConfigEnabled: 0'
- line: '| switchTcpInitialSendBufferSize: 32'
- line: '| switchTcpInitialReceiveBufferSize: 64'
- line: '| switchTcpAutoSendBufferSizeMax: 256'
- line: '| switchTcpAutoReceiveBufferSizeMax: 256'
- line: '| switchUdpSendBufferSize: 9'
- line: '| switchUdpReceiveBufferSize: 42'
- line: '| switchSocketBufferEfficiency: 4'
- line: '| switchSocketInitializeEnabled: 1'
- line: '| switchNetworkInterfaceManagerInitializeEnabled: 1'
- line: '| switchDisableHTCSPlayerConnection: 0'
- line: '| switchUseNewStyleFilepaths: 0'
- line: '| switchUseLegacyFmodPriorities: 0'
- line: '| switchUseMicroSleepForYield: 1'
- line: '| switchEnableRamDiskSupport: 0'
- line: '| switchMicroSleepForYieldTime: 25'
- line: '| switchRamDiskSpaceSize: 12'
- line: '| switchUpgradedPlayerSettingsToNMETA: 0'
- line: '| ps4NPAgeRating: 12'
- line: '| ps4NPTitleSecret: '
- line: '| ps4NPTrophyPackPath: '
- line: '| ps4ParentalLevel: 11'
- line: '| ps4ContentID: ED1633-NPXX51362_00-0000000000000000'
- line: '| ps4Category: 0'
- line: '| ps4MasterVersion: 01.00'
- line: '| ps4AppVersion: 01.00'
- line: '| ps4AppType: 0'
- line: '| ps4ParamSfxPath: '
- line: '| ps4VideoOutPixelFormat: 0'
- line: '| ps4VideoOutInitialWidth: 1920'
- line: '| ps4VideoOutBaseModeInitialWidth: 1920'
- line: '| ps4VideoOutReprojectionRate: 60'
- line: '| ps4PronunciationXMLPath: '
- line: '| ps4PronunciationSIGPath: '
- line: '| ps4BackgroundImagePath: '
- line: '| ps4StartupImagePath: '
- line: '| ps4StartupImagesFolder: '
- line: '| ps4IconImagesFolder: '
- line: '| ps4SaveDataImagePath: '
- line: '| ps4SdkOverride: '
- line: '| ps4BGMPath: '
- line: '| ps4ShareFilePath: '
- line: '| ps4ShareOverlayImagePath: '
- line: '| ps4PrivacyGuardImagePath: '
- line: '| ps4ExtraSceSysFile: '
- line: '| ps4NPtitleDatPath: '
- line: '| ps4RemotePlayKeyAssignment: -1'
- line: '| ps4RemotePlayKeyMappingDir: '
- line: '| ps4PlayTogetherPlayerCount: 0'
- line: '| ps4EnterButtonAssignment: 2'
- line: '| ps4ApplicationParam1: 0'
- line: '| ps4ApplicationParam2: 0'
- line: '| ps4ApplicationParam3: 0'
- line: '| ps4ApplicationParam4: 0'
- line: '| ps4DownloadDataSize: 0'
- line: '| ps4GarlicHeapSize: 2048'
- line: '| ps4ProGarlicHeapSize: 2560'
- line: '| playerPrefsMaxSize: 32768'
- line: '| ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ'
- line: '| ps4pnSessions: 1'
- line: '| ps4pnPresence: 1'
- line: '| ps4pnFriends: 1'
- line: '| ps4pnGameCustomData: 1'
- line: '| playerPrefsSupport: 0'
- line: '| enableApplicationExit: 0'
- line: '| resetTempFolder: 1'
- line: '| restrictedAudioUsageRights: 0'
- line: '| ps4UseResolutionFallback: 0'
- line: '| ps4ReprojectionSupport: 0'
- line: '| ps4UseAudio3dBackend: 0'
- line: '| ps4UseLowGarlicFragmentationMode: 1'
- line: '| ps4SocialScreenEnabled: 0'
- line: '| ps4ScriptOptimizationLevel: 2'
- line: '| ps4Audio3dVirtualSpeakerCount: 14'
- line: '| ps4attribCpuUsage: 0'
- line: '| ps4PatchPkgPath: '
- line: '| ps4PatchLatestPkgPath: '
- line: '| ps4PatchChangeinfoPath: '
- line: '| ps4PatchDayOne: 0'
- line: '| ps4attribUserManagement: 0'
- line: '| ps4attribMoveSupport: 0'
- line: '| ps4attrib3DSupport: 0'
- line: '| ps4attribShareSupport: 0'
- line: '| ps4attribExclusiveVR: 0'
- line: '| ps4disableAutoHideSplash: 0'
- line: '| ps4videoRecordingFeaturesUsed: 0'
- line: '| ps4contentSearchFeaturesUsed: 0'
- line: '| ps4CompatibilityPS5: 0'
- line: '| ps4AllowPS5Detection: 0'
- line: '| ps4GPU800MHz: 1'
- line: '| ps4attribEyeToEyeDistanceSettingVR: 0'
- line: '| ps4IncludedModules: []'
- line: '| ps4attribVROutputEnabled: 0'
- line: '| monoEnv: '
- line: '| splashScreenBackgroundSourceLandscape: {instanceID: 0}'
- line: '| splashScreenBackgroundSourcePortrait: {instanceID: 0}'
- line: '| blurSplashScreenBackground: 1'
- line: '| spritePackerPolicy: '
- line: '| webGLMemorySize: 32'
- line: '| webGLExceptionSupport: 1'
- line: '| webGLNameFilesAsHashes: 0'
- line: '| webGLShowDiagnostics: 0'
- line: '| webGLDataCaching: 1'
- line: '| webGLDebugSymbols: 0'
- line: '| webGLEmscriptenArgs: '
- line: '| webGLModulesDirectory: '
- line: '| webGLTemplate: APPLICATION:Default'
- line: '| webGLAnalyzeBuildSize: 0'
- line: '| webGLUseEmbeddedResources: 0'
- line: '| webGLCompressionFormat: 0'
- line: '| webGLWasmArithmeticExceptions: 0'
- line: '| webGLLinkerTarget: 1'
- line: '| webGLThreadsSupport: 0'
- line: '| webGLDecompressionFallback: 0'
- line: '| webGLInitialMemorySize: 32'
- line: '| webGLMaximumMemorySize: 2048'
- line: '| webGLMemoryGrowthMode: 2'
- line: '| webGLMemoryLinearGrowthStep: 16'
- line: '| webGLMemoryGeometricGrowthStep: 0.2'
- line: '| webGLMemoryGeometricGrowthCap: 96'
- line: '| webGLEnableWebGPU: 0'
- line: '| webGLPowerPreference: 2'
- line: '| webGLWebAssemblyTable: 0'
- line: '| webGLWebAssemblyBigInt: 0'
- line: '| webGLCloseOnQuit: 0'
- line: '| webWasm2023: 0'
- line: '| scriptingDefineSymbols:'
- line: '| Android: MIRROR;MIRROR_89_OR_NEWER;MIRROR_90_OR_NEWER;MIRROR_93_OR_NEWER;MIRROR_96_OR_NEWER;EDGEGAP_PLUGIN_SERVERS;DOTWEEN'
- line: '| EmbeddedLinux: DOTWEEN'
- line: '| GameCoreScarlett: DOTWEEN'
- line: '| GameCoreXboxOne: DOTWEEN'
- line: '| Kepler: DOTWEEN'
- line: '| LinuxHeadlessSimulation: DOTWEEN'
- line: '| Nintendo Switch: DOTWEEN'
- line: '| Nintendo Switch 2: DOTWEEN'
- line: '| PS4: DOTWEEN'
- line: '| PS5: DOTWEEN'
- line: '| QNX: DOTWEEN'
- line: '| Standalone: DOTWEEN'
- line: '| VisionOS: DOTWEEN'
- line: '| WebGL: DOTWEEN'
- line: '| Windows Store Apps: DOTWEEN'
- line: '| XboxOne: DOTWEEN'
- line: '| iPhone: DOTWEEN'
- line: '| tvOS: DOTWEEN'
- line: '| additionalCompilerArguments: {}'
- line: '| platformArchitecture: {}'
- line: '| scriptingBackend:'
- line: '| Android: 1'
- line: '| il2cppCompilerConfiguration: {}'
- line: '| il2cppCodeGeneration: {}'
- line: '| il2cppStacktraceInformation: {}'
- line: '| managedStrippingLevel: {}'
- line: '| incrementalIl2cppBuild: {}'
- line: '| suppressCommonWarnings: 1'
- line: '| allowUnsafeCode: 0'
- line: '| useDeterministicCompilation: 1'
- line: '| additionalIl2CppArgs: '
- line: '| scriptingRuntimeVersion: 1'
- line: '| gcIncremental: 1'
- line: '| gcWBarrierValidation: 0'
- line: '| apiCompatibilityLevelPerPlatform: {}'
- line: '| editorAssembliesCompatibilityLevel: 1'
- line: '| m_RenderingPath: 1'
- line: '| m_MobileRenderingPath: 1'
- line: '| metroPackageName: WalkInvest_Soccer'
- line: '| metroPackageVersion: '
- line: '| metroCertificatePath: '
- line: '| metroCertificatePassword: '
- line: '| metroCertificateSubject: '
- line: '| metroCertificateIssuer: '
- line: '| metroCertificateNotAfter: 0000000000000000'
- line: '| metroApplicationDescription: WalkInvest_Soccer'
- line: '| wsaImages: {}'
- line: '| metroTileShortName: '
- line: '| metroTileShowName: 0'
- line: '| metroMediumTileShowName: 0'
- line: '| metroLargeTileShowName: 0'
- line: '| metroWideTileShowName: 0'
- line: '| metroSupportStreamingInstall: 0'
- line: '| metroLastRequiredScene: 0'
- line: '| metroDefaultTileSize: 1'
- line: '| metroTileForegroundText: 2'
- line: '| metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628,
a: 0}'
- line: '| metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902,
b: 0.21568628, a: 1}'
- line: '| metroSplashScreenUseBackgroundColor: 0'
- line: '| syncCapabilities: 0'
- line: '| platformCapabilities: {}'
- line: '| metroTargetDeviceFamilies: {}'
- line: '| metroFTAName: '
- line: '| metroFTAFileTypes: []'
- line: '| metroProtocolName: '
- line: '| vcxProjDefaultLanguage: '
- line: '| XboxOneProductId: '
- line: '| XboxOneUpdateKey: '
- line: '| XboxOneSandboxId: '
- line: '| XboxOneContentId: '
- line: '| XboxOneTitleId: '
- line: '| XboxOneSCId: '
- line: '| XboxOneGameOsOverridePath: '
- line: '| XboxOnePackagingOverridePath: '
- line: '| XboxOneAppManifestOverridePath: '
- line: '| XboxOneVersion: 1.0.0.0'
- line: '| XboxOnePackageEncryption: 0'
- line: '| XboxOnePackageUpdateGranularity: 2'
- line: '| XboxOneDescription: '
- line: '| XboxOneLanguage:'
- line: '| - enus'
- line: '| XboxOneCapability: []'
- line: '| XboxOneGameRating: {}'
- line: '| XboxOneIsContentPackage: 0'
- line: '| XboxOneEnhancedXboxCompatibilityMode: 0'
- line: '| XboxOneEnableGPUVariability: 1'
- line: '| XboxOneSockets: {}'
- line: '| XboxOneSplashScreen: {instanceID: 0}'
- line: '| XboxOneAllowedProductIds: []'
- line: '| XboxOnePersistentLocalStorageSize: 0'
- line: '| XboxOneXTitleMemory: 8'
- line: '| XboxOneOverrideIdentityName: '
- line: '| XboxOneOverrideIdentityPublisher: '
- line: '| vrEditorSettings: {}'
- line: '| cloudServicesEnabled: {}'
- line: '| luminIcon:'
- line: '| m_Name: '
- line: '| m_ModelFolderPath: '
- line: '| m_PortalFolderPath: '
- line: '| luminCert:'
- line: '| m_CertPath: '
- line: '| m_SignPackage: 1'
- line: '| luminIsChannelApp: 0'
- line: '| luminVersion:'
- line: '| m_VersionCode: 1'
- line: '| m_VersionName: '
- line: '| hmiPlayerDataPath: '
- line: '| hmiForceSRGBBlit: 1'
- line: '| embeddedLinuxEnableGamepadInput: 0'
- line: '| hmiCpuConfiguration: '
- line: '| hmiLogStartupTiming: 0'
- line: '| qnxGraphicConfPath: '
- line: '| apiCompatibilityLevel: 6'
- line: '| captureStartupLogs: {}'
- line: '| activeInputHandler: 2'
- line: '| windowsGamepadBackendHint: 0'
- line: '| cloudProjectId: 267a39bf-cb72-4f17-bb8e-775bffe1a6a1'
- line: '| framebufferDepthMemorylessMode: 0'
- line: '| qualitySettingsNames: []'
- line: '| projectName: WalkInvest_Soccer'
- line: '| organizationId: sewmina7'
- line: '| cloudEnabled: 0'
- line: '| legacyClampBlendShapeWeights: 0'
- line: '| hmiLoadingImage: {instanceID: 0}'
- line: '| platformRequiresReadableAssets: 0'
- line: '| virtualTexturingSupportEnabled: 0'
- line: '| insecureHttpOption: 2'
- line: '| androidVulkanDenyFilterList: []'
- line: '| androidVulkanAllowFilterList: []'
- line: '| '
references:
version: 2
RefIds:
- rid: 1400252592429989960
type: {class: AndroidPlatformBuildSettings, ns: UnityEditor.Android, asm: UnityEditor.Android.Extensions}
data:
m_Development: 0
m_ConnectProfiler: 0
m_BuildWithDeepProfilingSupport: 0
m_AllowDebugging: 0
m_WaitForManagedDebugger: 0
m_ManagedDebuggerFixedPort: 0
m_ExplicitNullChecks: 0
m_ExplicitDivideByZeroChecks: 0
m_ExplicitArrayBoundsChecks: 0
m_CompressionType: 2
m_InstallInBuildFolder: 0
m_BuildSubtarget: 0
m_BuildSystem: 1
m_ExportAsGoogleAndroidProject: 0
m_DebugSymbolLevel: 1
m_DebugSymbolFormat: 5
m_CurrentDeploymentTargetId: __builtin__target_default
m_BuildType: 2
m_BuildAppBundle: 0
m_IPAddressToConnect:
m_SymlinkSources: 0
@@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0} m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
m_Name: New Linux Server Profile 1 m_Name: Server
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_AssetVersion: 1 m_AssetVersion: 1
m_BuildTarget: 24 m_BuildTarget: 24

Some files were not shown because too many files have changed in this diff Show More