270 lines
No EOL
9 KiB
C#
270 lines
No EOL
9 KiB
C#
/**********************************
|
|
* Author: Unknow *
|
|
* Update/Fixed/Refactoring: Expa *
|
|
* Script: PandoraChestLogger.cs *
|
|
* Optimized for ModernUO 0.15.4.18*
|
|
**********************************/
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Server;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
|
|
static class PandoraChestLogger
|
|
{
|
|
static readonly ConcurrentQueue<string> Q = new();
|
|
static CancellationTokenSource _cts;
|
|
static Task _worker;
|
|
static string _logFile;
|
|
static long _maxSize;
|
|
static int _flushInterval;
|
|
static readonly object _initLock = new();
|
|
static readonly JsonSerializerOptions _jsonOpts = new() { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
|
|
|
public static void Initialize()
|
|
{
|
|
lock (_initLock)
|
|
{
|
|
if (_worker != null && !_worker.IsCompleted) return;
|
|
var baseDir = AppDomain.CurrentDomain.BaseDirectory ?? Environment.CurrentDirectory;
|
|
var logsDir = Path.Combine(baseDir, "Logs");
|
|
try { Directory.CreateDirectory(logsDir); } catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.Initialize: CreateDirectory failed: {ex}"); } catch { } }
|
|
_logFile = Path.Combine(logsDir, "PandoraChestSpawns.log");
|
|
var cfg = PandoraChestConfig.Current ?? new PandoraChestConfig();
|
|
_maxSize = cfg.Logging?.MaxFileSizeBytes ?? (10 * 1024 * 1024);
|
|
_flushInterval = Math.Max(50, cfg.Logging?.FlushIntervalMs ?? 500);
|
|
_cts = new CancellationTokenSource();
|
|
_worker = Task.Run(() => Worker(_cts.Token));
|
|
}
|
|
}
|
|
|
|
static async Task Worker(CancellationToken token)
|
|
{
|
|
var batch = new List<string>();
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
try { await Task.Delay(_flushInterval, token).ConfigureAwait(false); } catch { break; }
|
|
batch.Clear();
|
|
while (Q.TryDequeue(out var l)) { batch.Add(l); if (batch.Count >= 1000) break; }
|
|
if (batch.Count == 0) continue;
|
|
try
|
|
{
|
|
RotateIfNeeded();
|
|
if (!string.IsNullOrEmpty(_logFile)) File.AppendAllLines(_logFile, batch, Encoding.UTF8);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try { Console.Error.WriteLine($"PandoraChestLogger.Worker AppendAllLines failed: {ex}"); } catch { }
|
|
foreach (var l in batch) try { Console.WriteLine(l); } catch { }
|
|
}
|
|
}
|
|
var rem = new List<string>();
|
|
while (Q.TryDequeue(out var l)) rem.Add(l);
|
|
if (rem.Count > 0)
|
|
{
|
|
try { RotateIfNeeded(); if (!string.IsNullOrEmpty(_logFile)) File.AppendAllLines(_logFile, rem, Encoding.UTF8); } catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.Worker final flush failed: {ex}"); } catch { } }
|
|
}
|
|
}
|
|
|
|
static void RotateIfNeeded()
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(_logFile)) return;
|
|
var fi = new FileInfo(_logFile);
|
|
if (fi.Exists && fi.Length > _maxSize)
|
|
{
|
|
var dest = _logFile + "." + DateTime.UtcNow.ToString("yyyyMMddHHmmss");
|
|
try { File.Move(_logFile, dest); } catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.RotateIfNeeded File.Move failed: {ex}"); } catch { } }
|
|
}
|
|
}
|
|
catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.RotateIfNeeded failed: {ex}"); } catch { } }
|
|
}
|
|
|
|
static string HashShort(string input)
|
|
{
|
|
if (string.IsNullOrEmpty(input)) return "(null)";
|
|
try
|
|
{
|
|
using var sha = SHA256.Create();
|
|
var h = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
|
|
var hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
|
|
return hex.Length >= 16 ? hex.Substring(0, 16) : hex;
|
|
}
|
|
catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.HashShort failed: {ex}"); } catch { } return "(err)"; }
|
|
}
|
|
|
|
static void EnqueueStructured(object obj)
|
|
{
|
|
try
|
|
{
|
|
var line = JsonSerializer.Serialize(obj, _jsonOpts);
|
|
Q.Enqueue(line);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try { Console.Error.WriteLine($"PandoraChestLogger.EnqueueStructured failed: {ex}"); } catch { }
|
|
}
|
|
}
|
|
|
|
public static void LogInfo(string tag, string context = null, Mobile who = null, object extra = null)
|
|
{
|
|
try
|
|
{
|
|
var entry = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
level = "INFO",
|
|
tag,
|
|
context,
|
|
player = who?.Name ?? "(null)",
|
|
accountHash = SafeAccountHash(who),
|
|
extra
|
|
};
|
|
EnqueueStructured(entry);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public static void LogWarn(string tag, string context = null, Mobile who = null, object extra = null)
|
|
{
|
|
try
|
|
{
|
|
var entry = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
level = "WARN",
|
|
tag,
|
|
context,
|
|
player = who?.Name ?? "(null)",
|
|
accountHash = SafeAccountHash(who),
|
|
extra
|
|
};
|
|
EnqueueStructured(entry);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public static void LogError(string tag, Exception ex, string context = null, Mobile who = null)
|
|
{
|
|
try
|
|
{
|
|
var entry = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
level = "ERROR",
|
|
tag,
|
|
context,
|
|
player = who?.Name ?? "(null)",
|
|
accountHash = SafeAccountHash(who),
|
|
exception = ex?.ToString()
|
|
};
|
|
EnqueueStructured(entry);
|
|
}
|
|
catch (Exception inner)
|
|
{
|
|
try { Console.Error.WriteLine($"PandoraChestLogger.LogError failed to enqueue: {inner}. Original: {tag} {context} {ex}"); } catch { }
|
|
}
|
|
}
|
|
|
|
static string SafeAccountHash(Mobile who)
|
|
{
|
|
try { var acc = who?.Account?.Username ?? who?.Account?.ToString() ?? "(null)"; return HashShort(acc); } catch { return "(err)"; }
|
|
}
|
|
|
|
public static void LogSpawn(Mobile who, Map map, Point3D loc, int level, double spawnChance)
|
|
{
|
|
try
|
|
{
|
|
if (!PandoraChestConfig.Current.Logging.Enabled) return;
|
|
}
|
|
catch { /* best-effort: fall through */ }
|
|
|
|
try
|
|
{
|
|
var entry = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
level = "INFO",
|
|
tag = "SPAWN",
|
|
player = who?.Name ?? "(null)",
|
|
accountHash = SafeAccountHash(who),
|
|
map = map?.ToString(),
|
|
loc = new { x = loc.X, y = loc.Y, z = loc.Z },
|
|
levelNumber = level,
|
|
chance = spawnChance
|
|
};
|
|
EnqueueStructured(entry);
|
|
}
|
|
catch (Exception ex) { LogError("PandoraChestLogger.LogSpawn", ex, null, who); }
|
|
}
|
|
|
|
public static void LogContents(Mobile who, int level, IEnumerable<string> items)
|
|
{
|
|
try
|
|
{
|
|
if (!PandoraChestConfig.Current.Logging.Enabled) return;
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
var entry = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
level = "INFO",
|
|
tag = "CONTENTS",
|
|
player = who?.Name ?? "(null)",
|
|
accountHash = SafeAccountHash(who),
|
|
levelNumber = level,
|
|
items = items
|
|
};
|
|
EnqueueStructured(entry);
|
|
}
|
|
catch (Exception ex) { LogError("PandoraChestLogger.LogContents", ex, null, who); }
|
|
}
|
|
|
|
public static void LogMagicDrop(Mobile who, int level, Item it)
|
|
{
|
|
try
|
|
{
|
|
if (!PandoraChestConfig.Current.Logging.Enabled) return;
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
var entry = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
level = "INFO",
|
|
tag = "MAGIC",
|
|
player = who?.Name ?? "(null)",
|
|
accountHash = SafeAccountHash(who),
|
|
levelNumber = level,
|
|
itemType = it?.GetType().Name,
|
|
itemName = it?.Name,
|
|
hue = it?.Hue ?? 0
|
|
};
|
|
EnqueueStructured(entry);
|
|
}
|
|
catch (Exception ex) { LogError("PandoraChestLogger.LogMagicDrop", ex, null, who); }
|
|
}
|
|
|
|
public static void Shutdown(int waitMs = 3000)
|
|
{
|
|
try
|
|
{
|
|
_cts?.Cancel();
|
|
_worker?.Wait(waitMs);
|
|
}
|
|
catch (Exception ex) { try { Console.Error.WriteLine($"PandoraChestLogger.Shutdown error: {ex}"); } catch { } }
|
|
}
|
|
} |