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
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
RestoreConfig(staged);
|
||||
@@ -6847,7 +6851,7 @@ namespace Oxide.Plugins
|
||||
if (failed.Count == 0)
|
||||
SettleConfigJob(job, true, false, null, null, null);
|
||||
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
|
||||
{
|
||||
@@ -6863,7 +6867,7 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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
|
||||
@@ -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>
|
||||
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>();
|
||||
@@ -10782,6 +10935,8 @@ namespace Oxide.Plugins
|
||||
entry.Started = true;
|
||||
entry.Name = baseName;
|
||||
entry.Difficulty = live.Difficulty;
|
||||
live.RunId = entry.RunId;
|
||||
live.Key = entry.Key;
|
||||
SaveWorld();
|
||||
}
|
||||
|
||||
@@ -10873,11 +11028,11 @@ namespace Oxide.Plugins
|
||||
_raids.Remove(live);
|
||||
|
||||
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
|
||||
// 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))
|
||||
{
|
||||
_world.Remove(ours);
|
||||
@@ -10888,8 +11043,9 @@ namespace Oxide.Plugins
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
LiveRaid told = live ?? new LiveRaid { At = location, Level = level, Difficulty = RaidDifficultyName(level), Pvp = allowPvp, Base = baseName, Id = ours };
|
||||
EmitFrame(RaidFrame(RaidEndedKind, told, null));
|
||||
LiveRaid told = live ?? new LiveRaid { At = location, Level = level, Difficulty = RaidDifficultyName(level), Pvp = allowPvp, Base = baseName };
|
||||
told.Id = ours;
|
||||
EmitFrame(RaidFrame(RaidEndedKind, told, entry));
|
||||
}
|
||||
|
||||
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["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;
|
||||
}
|
||||
@@ -11027,7 +11194,7 @@ namespace Oxide.Plugins
|
||||
continue;
|
||||
|
||||
int level = TupleItem(row, 3) is int l ? l : 0;
|
||||
_raids.Add(new LiveRaid
|
||||
var live = new LiveRaid
|
||||
{
|
||||
At = at,
|
||||
Level = level,
|
||||
@@ -11036,7 +11203,16 @@ namespace Oxide.Plugins
|
||||
Base = TupleItem(row, 14) as string,
|
||||
DespawnAtMs = TupleItem(row, 16) is DateTime d ? RaidMs(d) : 0L,
|
||||
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)
|
||||
@@ -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;
|
||||
|
||||
foreach (WorldCall c in _worldCalls.Values)
|
||||
@@ -19527,6 +19711,16 @@ namespace Oxide.Plugins
|
||||
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>
|
||||
private static EventSchedule ScheduleFor(string key)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user