Files
probably-decentralized/src/Decentralized/OtcClient.cs
T
ZHU MINCHAOandClaude Opus 5.5 9d95e1f760 换币客户改在交易终端成交:按比例部分成交、买币随机出价、弹出钱包
- 对话只剩"到终端上谈 / 这单我不做":交易终端打开柜台交易页(标签"柜台",只在有客户时出现),客户在柜台前等着
- 柜台交易页:客户来意、数量、市值、出价/要价、单价、地址(卖币可"查地址"进浏览器),25/50/75/100% 或手输数量部分成交(Otc.Part),成交 / 放弃
- 买币客户随机出价(OtcBidMin −5% … OtcBidMax +30%),去掉三档报价与成交概率(OtcPremium、OtcAccept);成交后弹出 PLEDGER,账单 Prefill 填好客户地址
- 卖币:成交后付现金、币进已连接的钱包;客户说最后一句离开(DialogUIManager.StartDialogue)
- 客户被打发或离开时交易作废;不再有"当晚未付作废"
- 增量翻译:只是和已删旧键同文的新键照常翻译
- 实测:买币客户 25% 部分成交(先在 Web3 换出 CRA),PLEDGER 预填地址签名,到账 +668,客户离开;涡轮增压器作用于矿架立刻挖一轮

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:46:14 +08:00

265 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 换币客户(游戏的对话顾客),由切到"换币客户"的暗灯招来,见 <see cref="OtcLamp"/> 与 <see cref="Otc"/>。
/// 客户说明来意后选"去终端交易":打开交易终端的柜台交易页(<see cref="TerminalWindow"/>),客户留在柜台前等着。
/// 在终端里定成交数量(可以只成交一部分):卖币的客户收现金、币直接进已连接的钱包(便衣会没收现金并记销赃罪证);
/// 买币的客户弹出 PLEDGER,收款地址和数量已填好,签名确认后客户付现金。客户走了(被打发、没成交)就作废。
/// 原版取顾客名字时也会调用工厂:只有在生成顾客的那段时间里(普通顾客回调之间,原版在这里按展示柜生成顾客)才生成报价、写链上账本。
/// </summary>
public static class OtcClient
{
public const string Id = OtcLamp.Target;
private static bool spawning;
/// <summary>正在柜台前、已经去终端交易的客户:报价、收款地址和顾客对象(用来判断他还在不在)。</summary>
public static OtcOffer Active;
public static string ActiveAddress;
private static StoreClient activeClient;
/// <summary>买币客户的账单:确认到账或作废之前一直记着。</summary>
private static WalletWindow.Bill pendingBill;
/// <summary>还没签名付款的换币账单:钱包没有终端账单时用它(见 <see cref="WalletWindow.Pending"/>);签名后清掉,没成交(挤出内存池)时放回来。</summary>
public static WalletWindow.Bill Standing;
public static void Subscribe()
{
StoreClientListDict.storeClientDict[Id] = Create;
ModHook.OnGenerateCustomerNormal += Open;
ModHook.OnGenerateCustomerLate += Close;
ModHook.OnGoingSleepEarly += Drop;
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "OTC client (test)", () => Log.Guard("换币客户(测试)", () =>
{
spawning = true;
StoreClient c;
try { c = Create(); } finally { spawning = false; }
PlayerStore.Instance.currentClientInstance = null;
PlayerStore.Instance.storeClientManager.clientStack.Insert(0, c);
PlayerStore.Instance.TryCallNextClient();
}));
}
public static void Unsubscribe()
{
StoreClientListDict.storeClientDict.Remove(Id);
ModHook.OnGenerateCustomerNormal -= Open;
ModHook.OnGenerateCustomerLate -= Close;
ModHook.OnGoingSleepEarly -= Drop;
}
/// <summary>这张账单是不是换币客户开的(终端不让它独占 Web3、灰网页)。</summary>
public static bool IsBill(WalletWindow.Bill bill) => bill != null && bill == pendingBill;
private static void Open(StoreClientManager _) => spawning = true;
private static void Close(StoreClientManager _) => spawning = false;
/// <summary>顾客表里的工厂。</summary>
private static StoreClient Create()
{
StoreClient c = null;
if (spawning && Session.Data != null && Unlock.OtcUnlocked) Log.Guard("换币客户上门", () => c = Real());
return c ?? Shell();
}
/// <summary>只有名字的顾客(原版取名字时用;生成失败时上门说一句就走)。</summary>
private static StoreClient Shell()
{
var c = Base();
c.mainDialogue.SetText(c.displayName, L.T("otc.refused")).SetEndAction(Leave);
Finish(c);
return c;
}
private static StoreClient Base() => new StoreClient
{
identifier = Id, displayName = L.T("otc.name"), spriteName = SpriteDict.GetRandomShadySpriteName(),
clientIntent = StoreClient.ClientIntent.DIALOGUE,
};
private static void Finish(StoreClient c)
{
ModHelper.SetClientSource(c, ModInfo.Id);
c.AddBasicDialog();
c.CompleteClientCreation();
}
private static StoreClient Real()
{
var d = Session.Data;
var side = Otc.PickSide(d.Rng);
bool ponero = Unlock.BlackUnlocked && BlackMarket.DeskOpen(StoreReputation.GetBMReputation());
var o = side == OtcSide.Sell ? Otc.MakeSell(Night.ActiveEvents(), ponero, Session.Price, Session.Rent(), d.Rng) : Otc.MakeBuy(Session.Price, Session.Rent(), d.Rng);
if (o == null) return null;
string addr = d.Address("otc:" + (++d.Otc.Seq));
var c = Base();
if (o.Side == OtcSide.Sell) WriteHistory(d, o, addr);
Build(c, o, addr);
Finish(c);
Log.Info(o.Side == OtcSide.Sell
? $"换币客户(卖币):{o.Qty:0.##} {o.Asset},市值 {o.Value:0},污点比例 {o.TaintRatio:0.00},要价 {o.Cash}{(o.Sting ? "(便衣)" : "")}"
: $"换币客户(买币):{o.Qty:0.##} {o.Asset},市值 {o.Value:0},出价 {o.Cash}");
Session.Save();
return c;
}
/// <summary>客户地址的来历写进链上账本(Ponero 不上账本):脏的来源是混币器、黑市平台和骗局地址,干净的是 Poinbase 和 DEX。</summary>
private static void WriteHistory(SaveData d, OtcOffer o, string addr)
{
if (o.Asset == Assets.Ponero) return;
var dirty = new List<string> { d.Address("mixer"), d.Address("platform") };
dirty.AddRange(d.Scams.ScamAddrs);
var clean = new List<string> { d.Address("poinbase"), d.Address("dex") };
foreach (var kv in Otc.History(o, dirty, clean, d.Rng)) d.Chain(kv.Key, addr, o.Asset, o.Qty * kv.Value);
}
/// <summary>对话:来意和报价,选"去终端交易"打开柜台交易页、客户留下等着;选"算了"客户走人。</summary>
private static void Build(StoreClient c, OtcOffer o, string addr)
{
var trade = new Dialogue();
trade.SetText("PLAYER", L.T("otc.say.trade")).NextDialogue().SetText(c.displayName, L.T("otc.wait"));
var no = new Dialogue();
no.SetText("PLAYER", L.T("otc.say.no")).NextDialogue().SetText(c.displayName, L.T("otc.refused")).SetEndAction(Leave);
string intro = o.Side == OtcSide.Buy ? "otc.intro.buy" : o.TaintRatio >= 1 || o.Asset == Assets.Ponero ? "otc.intro." + o.Asset : "otc.intro.sell";
string offer = o.Side == OtcSide.Buy
? L.T("otc.buy.ask", Fmt.Num(o.Qty), o.Asset, Fmt.Credits(o.Value), Fmt.Credits(o.Cash))
: L.T("otc.offer", Fmt.Num(o.Qty), o.Asset, Fmt.Credits(o.Value), Fmt.Credits(o.Cash), ChainAddress.Short(addr));
c.mainDialogue.SetText(c.displayName, L.T(intro, o.Asset)).NextDialogue()
.SetText(c.displayName, offer)
.AddChoice(DirectoryMaster.Item("yes"), L.T("otc.say.trade"), "", trade, () => Begin(c, o, addr))
.AddChoice(DirectoryMaster.Item("cancel"), L.T("otc.say.no"), "", no, null);
}
/// <summary>去终端交易:记下在场的客户,打开交易终端的柜台交易页。</summary>
private static void Begin(StoreClient c, OtcOffer o, string addr)
{
Log.Guard("换币客户去终端交易", () =>
{
Active = o; ActiveAddress = addr; activeClient = c;
TerminalWindow.OpenOtc();
});
}
/// <summary>客户还在柜台前吗(没被打发、没走)。不在了就作废这笔交易。</summary>
public static bool Present()
{
if (Active == null) return false;
var cur = PlayerStore.Instance?.currentClientInstance?.GetClientBlueprint();
if (cur != null && cur == activeClient) return true;
Log.Info("换币客户已经离开,交易作废");
Drop();
return false;
}
/// <summary>
/// 终端里点"成交":qty 是成交的数量(不超过报价)。卖币:付现金、币进已连接的钱包;买币:开账单、弹出钱包(地址已填好)。
/// 返回终端显示的消息(null 表示弹出了钱包,等签名)。
/// </summary>
public static string Deal(double qty)
{
if (!Present()) return L.T("otc.msg.gone");
var part = Otc.Part(Active, qty);
if (part == null) return L.T("msg.amount");
return part.Side == OtcSide.Sell ? Take(part) : Give(part);
}
/// <summary>收下客户卖的币:没连钱包、现金不够就作罢;便衣没收现金、记销赃罪证;否则付现金、币进钱包。客户随后离开。</summary>
private static string Take(OtcOffer o)
{
var d = Session.Data;
var ps = PlayerStore.Instance;
if (!d.Connected) return L.T("web3.err.noconn");
if (ps.playerCash < o.Cash) return L.T("otc.msg.cash");
string msg = null;
Log.Guard("换币客户成交(卖币)", () =>
{
ModHelper.AddCredit(-o.Cash, false);
if (o.Sting)
{
ps.secData.CommitCrime("FENCING", (int)Math.Min(Tuning.CrimeEventCap, o.Cash));
Say(L.T("otc.sting"));
msg = L.T("otc.msg.sting", Fmt.Credits(o.Cash));
Log.Info($"换币客户:便衣,现金 {o.Cash} 没收,记销赃罪证");
}
else
{
double over = Otc.Receive(d.Hot, o, Session.Price);
d.Chain(ActiveAddress, d.Hot.Address, o.Asset, o.Qty);
d.Otc.Record(o.Cash);
msg = L.T("otc.msg.bought", Fmt.Num(o.Qty), o.Asset, Fmt.Credits(o.Cash)) + TaintCrime.Commit(over);
Say(L.T("otc.done"));
Log.Info($"换币客户:{o.Qty:0.##} {o.Asset} 进 {WalletWindow.NameOf(d.Hot)},付现金 {o.Cash},钱包污点 {d.Hot.Taint:0},超出 {over:0}");
}
Drop();
Session.Save();
});
return msg;
}
/// <summary>卖币给客户:开账单(客户地址已填好),弹出钱包签名;确认后客户付现金、离开。</summary>
private static string Give(OtcOffer o)
{
double qty = WalletWindow.RoundUp(o.Qty);
string asset = o.Asset;
int cash = o.Cash;
WalletWindow.Bill bill = null;
bill = new WalletWindow.Bill
{
Address = ActiveAddress, Asset = asset, Amount = qty, Prefill = true, Summary = L.T("pay.otc", WalletWindow.Amt(qty), asset, Fmt.Credits(cash)),
Execute = pw => Log.Guard("换币客户到账", () =>
{
pw.Spend(asset, qty, Session.Price);
ModHelper.AddCredit(cash, true);
Session.Data.Otc.Record(cash);
if (pendingBill == bill) pendingBill = null;
if (Active != null && Present()) { Say(L.T("otc.paid")); Active = null; ActiveAddress = null; activeClient = null; }
TerminalWindow.Refresh(L.T("otc.msg.sold", WalletWindow.Amt(qty), asset, Fmt.Credits(cash)));
Log.Info($"换币客户:{qty} {asset} 到账,收现金 {cash}");
Session.Save();
}),
};
pendingBill = Standing = bill;
WalletWindow.OpenForBill(bill);
Log.Info($"换币客户:卖出 {qty} {asset},现金 {cash},等签名");
return null;
}
/// <summary>客户说最后一句,然后离开。</summary>
private static void Say(string line)
{
var c = activeClient;
if (c == null || DialogUIManager.Instance == null) return;
var dl = new Dialogue();
dl.SetText(c.displayName, line).SetEndAction(Leave);
DialogUIManager.Instance.StartDialogue(dl);
}
/// <summary>放弃(终端的"放弃"):客户说一句走人,交易作废。</summary>
public static void Cancel()
{
if (Present()) Say(L.T("otc.refused"));
Drop();
}
/// <summary>这位客户的交易结束(成交、作废、离开,或者睡觉前):还没签名的账单一起作废;已经签名、还在确认中的照常到账。</summary>
private static void Drop()
{
Active = null; ActiveAddress = null; activeClient = null;
Void();
}
/// <summary>作废换币账单(终端的"作废"、客户离开、读档)。</summary>
public static void Void()
{
if (Standing != null && WalletWindow.Pending == Standing) WalletWindow.Pending = null;
Standing = null;
if (pendingBill != null && Standing == null) pendingBill = null;
}
private static void Leave() => DialogUIManager.Instance.CurrentClientLeave();
}
}