Files
probably-decentralized/src/Decentralized/Night.cs
T
2026-11-03 23:32:50 +08:00

218 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 每天的推进:开门时日内波动和 NPC 大户;睡觉时(游戏 EndNight 的最后)挖矿、损耗与暴毙、全网、稳定币、行情结算,写晨间日志,存档。
/// 矿架自己在夜间早段扣电并登记显卡(Items.NightDraw),这里只结算。
/// </summary>
public static class Night
{
public static void Subscribe()
{
ModHook.OnShutterOpenedLate += OnOpen;
ModHook.OnGoingSleepEarly += OnSleepEarly;
ModHook.OnGoingSleepLate += OnSleepLate;
}
public static void Unsubscribe()
{
ModHook.OnShutterOpenedLate -= OnOpen;
ModHook.OnGoingSleepEarly -= OnSleepEarly;
ModHook.OnGoingSleepLate -= OnSleepLate;
}
private static bool Active => Session.Data != null && Unlock.IsUnlocked;
/// <summary>游戏新闻事件与派系行动 → 行情事件。</summary>
public static List<MarketEvent> ActiveEvents()
{
var list = new List<MarketEvent>();
var mgr = StoreStation.Instance?.storeEventManager;
if (mgr == null) return list;
foreach (var e in mgr.GetActiveEvents())
{
switch (e.identifier)
{
case "stationWideBlackout": case "powerShortage": case "powerPlantMaintenance": list.Add(MarketEvent.Blackout); break;
case "crackdown": case "sec_martial_law": list.Add(MarketEvent.Crackdown); break;
case "bm_gang_war": list.Add(MarketEvent.GangWar); break;
case "rev_raid_upper_level": list.Add(MarketEvent.RevRaidUpper); break;
case "ll_riot": case "ll_worker_strike": list.Add(MarketEvent.LowerUnrest); break;
}
}
return list.Distinct().ToList();
}
private static void OnOpen()
{
if (!Active) return;
Log.Guard("开门行情", () =>
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
d.Market.Open(ActiveEvents(), d.Rng, day);
d.Market.Whale(d.Rng);
if (!d.OpeningNewsShown)
{
News.Show("pd_news_open", L.T("news.open.title"), L.T("news.open.desc"), 2);
d.OpeningNewsShown = true;
}
Session.Save();
});
}
private static void OnSleepEarly()
{
Session.TonightCards.Clear();
Session.TonightRigs = Session.TonightPoweredRigs = 0;
Session.TonightKj = 0;
}
private static void OnSleepLate()
{
if (!Active) return;
Log.Guard("夜间结算", Settle);
}
/// <summary>链上的夜间事件:网络拥堵、地址投毒的 0 币转入、钓鱼公告。</summary>
private static void ChainNight(SaveData d, int day)
{
if (d.GasPhase < 0) d.GasPhase = d.Rng.Range(0, Tuning.TxGasPeriod - 1);
d.Gas = TxRules.Gas(day + 1, d.GasPhase, d.Rng);
Log.Info($"明天 gas ×{d.Gas:0.00}");
if (day >= d.CongestedUntil && d.Rng.Chance(Tuning.TxCongestionChance))
{
d.CongestedFrom = day + 1;
d.CongestedUntil = day + d.Rng.Range(1, Tuning.TxCongestionDaysMax);
News.Show("pd_news_congest_" + day, L.T("news.congest.title"), L.T("news.congest.desc"), d.CongestedUntil - day);
}
foreach (var p in d.PoisonPlans.Where(x => x.Day <= day + 1).ToList())
{
d.PoisonPlans.Remove(p);
var w = d.Own(p.Wallet);
if (w == null) continue;
string fake = ChainAddress.Lookalike(p.Target, d.Rng);
TxRules.Record(w.History, new TxRecord { Addr = fake, Asset = Assets.PSC, Amount = 0, Day = day + 1, In = true });
Log.Info($"地址投毒:{p.Target} 的相似地址 {fake} 向 {w.Address} 转入 0 币");
}
if (d.Rng.Chance(Tuning.TxPhishChance))
{
string fake = ChainAddress.Make(d.Rng.NextUInt());
if (Unlock.BlackUnlocked && d.Rng.Chance(0.5)) d.Black.Forum.Add("phish:" + fake);
else News.Show("pd_news_phish_" + day, L.T("news.phish.title"), L.T("news.phish.desc", fake), 2);
Log.Info("钓鱼公告:" + fake);
}
}
private static void Settle()
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
var events = ActiveEvents();
Log.Info($"夜间事件:{string.Join(",", events)}(新增 {string.Join(",", events.Where(e => !d.ActiveEvents.Contains(e)))}),黑市声望 {StoreReputation.GetBMReputation()}");
bool wasSanctioned = d.Mixer.Sanctioned(day);
foreach (var ev in events.Where(e => !d.ActiveEvents.Contains(e)))
{
d.Stable.OnEventStart(ev, day, d.Rng); d.Mixer.OnEvent(ev, day, d.Rng);
// 镇压开始:黑市为换取放行,按声望出卖一批 CRT 地址
if (ev == MarketEvent.Crackdown && Stablecoins.CrtFrozenByCrackdown(StoreReputation.GetBMReputation(), d.Rng))
{
double frozen = d.Wallets.Sum(w => w.Freeze(Assets.CRT, Tuning.CrtFreezeShare, day + Tuning.CrtUnfreezeAfter));
if (frozen > 0)
{
PlayerStore.Instance.AddNightLog(L.T("nightlog.crt_freeze", Fmt.Num(frozen), day + Tuning.CrtUnfreezeAfter), "cc7755");
News.Show("pd_news_crtfreeze_" + day, L.T("news.crtfreeze.title"), L.T("news.crtfreeze.desc"), 3);
}
}
}
foreach (var w in d.Wallets)
foreach (var t in w.Thaw(day)) PlayerStore.Instance.AddNightLog(L.T("nightlog.thaw", Fmt.Num(t.Value), t.Key));
if (!wasSanctioned && d.Mixer.Sanctioned(day) && Unlock.MixerUnlocked) News.Show("pd_news_sanction_" + day, L.T("news.sanction.title"), L.T("news.sanction.desc"), 3);
d.ActiveEvents = events;
if (Unlock.BlackUnlocked) BlackDelivery.Night(d, day, events.Contains(MarketEvent.Crackdown));
d.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1);
var units = Session.TonightCards.Select(c => (item: c.Key, unit: Items.ToUnit(c.Key), bonus: c.Value)).ToList();
double hash = units.Sum(x => x.unit.Hash(x.bonus));
double mined = d.Network.Mine(hash);
if (mined > 0) d.Hot.Add(Assets.PSC, mined);
var report = new NightReport
{
Day = day, Rigs = Session.TonightRigs, PoweredRigs = Session.TonightPoweredRigs, Kj = Session.TonightKj,
Hash = hash, NetworkHash = d.Network.NpcHash * d.Network.Availability + hash, Mined = mined,
};
foreach (var (item, unit, bonus) in units)
{
bool died = unit.Night(bonus, d.Rng);
Items.Store(item, unit);
if (died) report.Burnt.Add(item.name);
}
double rewardBefore = d.Network.Reward;
d.Network.Night(day, d.Market.Price, hash);
if (d.Network.Reward < rewardBefore) d.Market.OnHalving(d.Rng);
if (d.Network.Reward < rewardBefore) News.Show("pd_news_halving_" + day, L.T("news.halving.title"), L.T("news.halving.desc"), 2);
double crcFactor = d.Stable.Night(day, d.Rng, 0);
if (crcFactor < 1)
foreach (var w in d.Wallets.Append(d.Poinbase))
if (w.Get(Assets.CRC) > 0) w.Balances[Assets.CRC] = w.Get(Assets.CRC) * crcFactor;
d.Market.UpdateSupply(day);
if (Tuning.HalvingDays - day % Tuning.HalvingDays == 3) News.Show("pd_news_prehalving_" + day, L.T("news.prehalving.title"), L.T("news.prehalving.desc"), 3);
d.Market.Night(events, d.Rng, day, Session.Price);
report.Price = d.Market.Price;
d.LastNight = report;
d.Day = day;
var ps = PlayerStore.Instance;
if (mined > 0) ps.AddNightLog(L.T("nightlog.mined", Fmt.Num(mined)));
foreach (var m in d.Mixer.Night(d.WalletAt, day, Session.Price, d.Rng))
{
Log.Info($"混币器:{(m.Rugged ? "跑路" : "提回")} {m.Amount:0.##} {m.Asset},污点 {m.TaintBefore:0} → {m.TaintAfter:0},标记 {m.Flagged}");
if (m.Rugged) ps.AddNightLog(L.T("nightlog.mixer_rug", Fmt.Num(m.Amount), m.Asset), "cc7755");
else
{
string line = L.T("nightlog.mixer", Fmt.Num(m.Amount), m.Asset, Fmt.Credits(m.TaintBefore), Fmt.Credits(m.TaintAfter)) + (m.Flagged ? L.T("nightlog.mixer_flag") : "");
if (m.Flagged) ps.AddNightLog(line, "cc7755"); else ps.AddNightLog(line);
}
}
// 被投毒的硬件钱包:钱够多以后,别人用手里的助记词把它转空
foreach (var w in d.Wallets.Where(x => x.Poisoned))
{
double v = w.Value(Session.Price);
if (!d.Rng.Chance(Wallet.DrainChance(v))) continue;
w.Drain(Session.Price);
Log.Info($"投毒钱包 {w.Address} 被转空:{v:0}");
ps.AddNightLog(L.T("nightlog.drained", WalletWindow.NameOf(w), Fmt.Credits(v)), "cc7755");
}
ChainNight(d, day);
foreach (var name in report.Burnt) ps.AddNightLog(L.T("nightlog.burnt", name), "cc7755");
int unpowered = report.Rigs - report.PoweredRigs;
if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755");
if (d.CoinSalesToday > 0) ps.AddNightLog(L.T("nightlog.coinpay", Fmt.Credits(d.CoinSalesToday)));
d.CoinSalesToday = 0;
Log.Info($"夜间结算第 {day} 天:钱包污点 {d.Hot.Taint:0},CRT {d.Hot.Get(Assets.CRT):0}(冻结 {(d.Hot.Frozen.TryGetValue(Assets.CRT, out var fz) ? fz : 0):0}),混币器在途 {d.Mixer.Pending.Count},制裁至 {d.Mixer.SanctionedUntil}");
Session.Save();
}
}
/// <summary>mod 新闻:不带价格修正的新闻事件,和原版新闻一样出现在报纸上。</summary>
public static class News
{
public static void Show(string id, string title, string text, int days)
{
var mgr = StoreStation.Instance?.storeEventManager;
if (mgr == null || mgr.IsEventActive(id)) return;
mgr.ActivateEvent(new StoreEvent
{
identifier = id, newsName = title, displayName = title, newsDescription = text,
duration = days, eventType = StoreEvent.EventType.COSMETIC, eventArea = StoreEvent.EventArea.ALL,
importance = 2, cooldown = 0, negociationDatas = new List<NegociationData>(),
});
}
}
}