fix(bridge): the last fixes' bridge half (W3, W9, W12) #43
@@ -6792,6 +6792,10 @@ namespace Oxide.Plugins
|
|||||||
ReloadsBefore = helper.Call("ReloadsFinished") is int done ? done : 0
|
ReloadsBefore = helper.Call("ReloadsFinished") is int done ? done : 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// RaidableBases' own words on a profile it cannot read are logged during the reload,
|
||||||
|
// so the log is read from here when it is rolled back (W9).
|
||||||
|
MarkLogPosition(job);
|
||||||
|
|
||||||
if (!(helper.Call("ReloadProfiles") is bool asked) || !asked)
|
if (!(helper.Call("ReloadProfiles") is bool asked) || !asked)
|
||||||
{
|
{
|
||||||
RestoreConfig(staged);
|
RestoreConfig(staged);
|
||||||
@@ -6847,7 +6851,7 @@ namespace Oxide.Plugins
|
|||||||
if (failed.Count == 0)
|
if (failed.Count == 0)
|
||||||
SettleConfigJob(job, true, false, null, null, null);
|
SettleConfigJob(job, true, false, null, null, null);
|
||||||
else
|
else
|
||||||
RollBackRaidProfiles(job, $"RaidableBases did not load profile {string.Join(", ", failed.ToArray())}", LogTail());
|
RollBackRaidProfiles(job, $"RaidableBases did not load profile {string.Join(", ", failed.ToArray())}", RaidProfileErrors(job, failed));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -6863,7 +6867,7 @@ namespace Oxide.Plugins
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (job.Phase == ConfigPhase.Edit)
|
if (job.Phase == ConfigPhase.Edit)
|
||||||
RollBackRaidProfiles(job, $"RaidableBases' profile reload did not finish within {ConfigReloadCeilingSeconds:0}s", LogTail());
|
RollBackRaidProfiles(job, $"RaidableBases' profile reload did not finish within {ConfigReloadCeilingSeconds:0}s", RaidProfileErrors(job, null));
|
||||||
else
|
else
|
||||||
SettleConfigJob(job, false, true, false, $"{job.RollbackReason}; the restored profiles did not finish reloading either", job.RollbackLog);
|
SettleConfigJob(job, false, true, false, $"{job.RollbackReason}; the restored profiles did not finish reloading either", job.RollbackLog);
|
||||||
}
|
}
|
||||||
@@ -6884,6 +6888,148 @@ namespace Oxide.Plugins
|
|||||||
SettleConfigJob(job, false, true, false, $"{reason}; the files are back, but RaidableBases would not reload them", log);
|
SettleConfigJob(job, false, true, false, $"{reason}; the files are back, but RaidableBases would not reload them", log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The most lines of one of RaidableBases' entries a rollback carries: its file, the exception, its message.</summary>
|
||||||
|
private const int RaidProfileErrorLinesMax = 6;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A log line that begins an entry: Oxide's <c>06:50 [Info] </c> or Carbon's
|
||||||
|
/// <c>[2026.10.21 18:45:45] [INFO] </c>. Anything else continues the entry above it.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly System.Text.RegularExpressions.Regex LogEntryStart =
|
||||||
|
new System.Text.RegularExpressions.Regex(@"^(\d{2}:\d{2} |\[\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}\] )\[", System.Text.RegularExpressions.RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main thread. What RaidableBases itself logged since the profile write began (W9), for
|
||||||
|
/// a rollback: an unreadable profile is one entry, the file's path and then the exception
|
||||||
|
/// (<c>Could not convert string to integer: not a number. Path 'Difficulty Level', line 3</c>),
|
||||||
|
/// and that much is carried, without the stack. With <paramref name="failed"/>, only the
|
||||||
|
/// entries naming one of those profiles; with none of those, or none asked for, every
|
||||||
|
/// entry of its own since the write. Null when it said nothing: the reason line stands
|
||||||
|
/// alone rather than borrowing the server log's tail, which is other plugins' lines.
|
||||||
|
/// </summary>
|
||||||
|
private string RaidProfileErrors(ConfigWriteJob job, List<string> failed)
|
||||||
|
{
|
||||||
|
string text = LogSinceMark(job);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Plugin rb = RaidableBasesPlugin();
|
||||||
|
var tags = new List<string> { "[RaidableBases]" };
|
||||||
|
|
||||||
|
if (rb != null && !string.IsNullOrEmpty(rb.Title))
|
||||||
|
tags.Add("[" + rb.Title + "]");
|
||||||
|
|
||||||
|
var named = new StringBuilder();
|
||||||
|
var all = new StringBuilder();
|
||||||
|
StringBuilder entry = null;
|
||||||
|
bool namesFailed = false;
|
||||||
|
int kept = 0;
|
||||||
|
|
||||||
|
Action close = () =>
|
||||||
|
{
|
||||||
|
if (entry == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
all.Append(entry);
|
||||||
|
|
||||||
|
if (namesFailed)
|
||||||
|
named.Append(entry);
|
||||||
|
|
||||||
|
entry = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (string raw in text.Split('\n'))
|
||||||
|
{
|
||||||
|
string line = raw.TrimEnd('\r');
|
||||||
|
|
||||||
|
if (LogEntryStart.IsMatch(line))
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
|
||||||
|
if (!tags.Exists(t => line.IndexOf(t, StringComparison.OrdinalIgnoreCase) >= 0))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
entry = new StringBuilder(line).Append('\n');
|
||||||
|
kept = 1;
|
||||||
|
namesFailed = failed != null && failed.Exists(n => line.IndexOf(n + ".json", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stack is the plugin's, not the operator's: the message above it is the error.
|
||||||
|
if (entry == null || kept >= RaidProfileErrorLinesMax || line.TrimStart().StartsWith("at ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (entry != null)
|
||||||
|
kept = RaidProfileErrorLinesMax;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.Trim().Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
entry.Append(line).Append('\n');
|
||||||
|
kept++;
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
|
||||||
|
string found = (named.Length > 0 ? named : all).ToString().TrimEnd('\n');
|
||||||
|
|
||||||
|
if (found.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return found.Length > ConfigLogTailBytes ? found.Substring(0, ConfigLogTailBytes) : found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main thread. The log written since <see cref="MarkLogPosition"/>, at most
|
||||||
|
/// <c>ConfigLogPollMaxBytes</c>, without moving the job's own reading place. A log that
|
||||||
|
/// rotated since is read on from its mark and then from the new file's start.
|
||||||
|
/// </summary>
|
||||||
|
private string LogSinceMark(ConfigWriteJob job)
|
||||||
|
{
|
||||||
|
var text = new StringBuilder();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string newest = NewestLogFile();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(job.LogPath) && File.Exists(job.LogPath))
|
||||||
|
text.Append(ReadLogFrom(job.LogPath, job.LogOffset, ConfigLogPollMaxBytes));
|
||||||
|
|
||||||
|
if (newest != null && !string.Equals(newest, job.LogPath, StringComparison.OrdinalIgnoreCase) && text.Length < ConfigLogPollMaxBytes)
|
||||||
|
text.Append(ReadLogFrom(newest, 0, ConfigLogPollMaxBytes - text.Length));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// A log that cannot be read leaves the rollback its reason line.
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadLogFrom(string path, long offset, int max)
|
||||||
|
{
|
||||||
|
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
|
{
|
||||||
|
// A log shorter than the mark was truncated: read it from the top.
|
||||||
|
if (stream.Length < offset)
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
int take = (int)Math.Min(stream.Length - offset, max);
|
||||||
|
|
||||||
|
if (take <= 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
stream.Seek(offset, SeekOrigin.Begin);
|
||||||
|
var buffer = new byte[take];
|
||||||
|
int read = stream.Read(buffer, 0, take);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main thread. If a write touched this plugin's own config, take the one setting that
|
/// Main thread. If a write touched this plugin's own config, take the one setting that
|
||||||
/// is safe to change live — <c>EventsEnabled</c> — from what was written. Returns
|
/// is safe to change live — <c>EventsEnabled</c> — from what was written. Returns
|
||||||
@@ -10525,6 +10671,13 @@ namespace Oxide.Plugins
|
|||||||
|
|
||||||
/// <summary>The run's id for it (a world registry id), or null for a base no run made.</summary>
|
/// <summary>The run's id for it (a world registry id), or null for a base no run made.</summary>
|
||||||
public string Id;
|
public string Id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The run and step key behind <see cref="Id"/>, kept here too: a teardown takes the
|
||||||
|
/// base off the record before its despawn raises <c>Ended</c> (W12).
|
||||||
|
/// </summary>
|
||||||
|
public string RunId;
|
||||||
|
public string Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<LiveRaid> _raids = new List<LiveRaid>();
|
private readonly List<LiveRaid> _raids = new List<LiveRaid>();
|
||||||
@@ -10782,6 +10935,8 @@ namespace Oxide.Plugins
|
|||||||
entry.Started = true;
|
entry.Started = true;
|
||||||
entry.Name = baseName;
|
entry.Name = baseName;
|
||||||
entry.Difficulty = live.Difficulty;
|
entry.Difficulty = live.Difficulty;
|
||||||
|
live.RunId = entry.RunId;
|
||||||
|
live.Key = entry.Key;
|
||||||
SaveWorld();
|
SaveWorld();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10873,11 +11028,11 @@ namespace Oxide.Plugins
|
|||||||
_raids.Remove(live);
|
_raids.Remove(live);
|
||||||
|
|
||||||
string ours = RaidTagAt(location) ?? live?.Id;
|
string ours = RaidTagAt(location) ?? live?.Id;
|
||||||
WorldEntry entry;
|
WorldEntry entry = null;
|
||||||
|
|
||||||
// A run's base the plugin ended first, on its own timer or because it was raided: the
|
// A run's base the plugin ended first, on its own timer or because it was raided: the
|
||||||
// run's resource is marked expired (D170). One the run's teardown despawned is already
|
// run's resource is marked expired (D170). One the run's teardown despawned is already
|
||||||
// off the record.
|
// off the record, and its id, run and key come from the live base instead.
|
||||||
if (ours != null && _world.TryGetValue(ours, out entry))
|
if (ours != null && _world.TryGetValue(ours, out entry))
|
||||||
{
|
{
|
||||||
_world.Remove(ours);
|
_world.Remove(ours);
|
||||||
@@ -10888,8 +11043,9 @@ namespace Oxide.Plugins
|
|||||||
if (!WorldEventsLive())
|
if (!WorldEventsLive())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LiveRaid told = live ?? new LiveRaid { At = location, Level = level, Difficulty = RaidDifficultyName(level), Pvp = allowPvp, Base = baseName, Id = ours };
|
LiveRaid told = live ?? new LiveRaid { At = location, Level = level, Difficulty = RaidDifficultyName(level), Pvp = allowPvp, Base = baseName };
|
||||||
EmitFrame(RaidFrame(RaidEndedKind, told, null));
|
told.Id = ours;
|
||||||
|
EmitFrame(RaidFrame(RaidEndedKind, told, entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRaidableBasePurchased(string userId, UnityEngine.Vector3 location, string grid, int level, bool allowPvp, string baseName,
|
private void OnRaidableBasePurchased(string userId, UnityEngine.Vector3 location, string grid, int level, bool allowPvp, string baseName,
|
||||||
@@ -10948,6 +11104,17 @@ namespace Oxide.Plugins
|
|||||||
frame["runId"] = entry.RunId;
|
frame["runId"] = entry.RunId;
|
||||||
frame["key"] = entry.Key;
|
frame["key"] = entry.Key;
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(live.Id))
|
||||||
|
{
|
||||||
|
// A run's base no longer on the record: a teardown's despawn (W12).
|
||||||
|
frame["id"] = live.Id;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(live.RunId))
|
||||||
|
frame["runId"] = live.RunId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(live.Key))
|
||||||
|
frame["key"] = live.Key;
|
||||||
|
}
|
||||||
|
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
@@ -11027,7 +11194,7 @@ namespace Oxide.Plugins
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
int level = TupleItem(row, 3) is int l ? l : 0;
|
int level = TupleItem(row, 3) is int l ? l : 0;
|
||||||
_raids.Add(new LiveRaid
|
var live = new LiveRaid
|
||||||
{
|
{
|
||||||
At = at,
|
At = at,
|
||||||
Level = level,
|
Level = level,
|
||||||
@@ -11036,7 +11203,16 @@ namespace Oxide.Plugins
|
|||||||
Base = TupleItem(row, 14) as string,
|
Base = TupleItem(row, 14) as string,
|
||||||
DespawnAtMs = TupleItem(row, 16) is DateTime d ? RaidMs(d) : 0L,
|
DespawnAtMs = TupleItem(row, 16) is DateTime d ? RaidMs(d) : 0L,
|
||||||
Id = RaidTagAt(at)
|
Id = RaidTagAt(at)
|
||||||
});
|
};
|
||||||
|
WorldEntry entry;
|
||||||
|
|
||||||
|
if (live.Id != null && _world.TryGetValue(live.Id, out entry))
|
||||||
|
{
|
||||||
|
live.RunId = entry.RunId;
|
||||||
|
live.Key = entry.Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
_raids.Add(live);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -19475,6 +19651,14 @@ namespace Oxide.Plugins
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The game runs one Travelling Vendor at a time, and its event quietly starts nothing
|
||||||
|
// while one is out (W3). Said, not counted, and not kept: a retry looks again.
|
||||||
|
if (ev == "travelling_vendor" && TravellingVendorOut())
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "already-out", "a Travelling Vendor is already out, and the game runs one at a time; nothing was called");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int used = 0;
|
int used = 0;
|
||||||
|
|
||||||
foreach (WorldCall c in _worldCalls.Values)
|
foreach (WorldCall c in _worldCalls.Values)
|
||||||
@@ -19527,6 +19711,16 @@ namespace Oxide.Plugins
|
|||||||
EmitFrame(frame);
|
EmitFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Main thread. Whether a Travelling Vendor is on the map now.</summary>
|
||||||
|
private static bool TravellingVendorOut()
|
||||||
|
{
|
||||||
|
foreach (TravellingVendor v in UnityEngine.Object.FindObjectsOfType<TravellingVendor>())
|
||||||
|
if (v != null && !v.IsDestroyed)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>The game's own schedule for an event, enabled or not: an operator who switched one off may still call it.</summary>
|
/// <summary>The game's own schedule for an event, enabled or not: an operator who switched one off may still call it.</summary>
|
||||||
private static EventSchedule ScheduleFor(string key)
|
private static EventSchedule ScheduleFor(string key)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user