Stage 4: black market orders, escrow, delivery, seller scams, platform rug

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 19:48:26 +08:00
1 parent f43cd487e5
commit 6bc479f710
12 files changed
+592 -35

No files matched your search

+5 -1
View File
@@ -8,7 +8,11 @@
- 现货柜台:每周刷新一批违禁品(游戏已有的 32 种,低 / 中 / 高三档随声望 10 / 40 / 80 放开;件数 4 + 声望/40),价格 = 游戏基础价 × 1.3,只收 PSC / CRT / CRA / Ponero,不收 CRC。付出去的币连同按比例的污点留在卖家那边,货放进储藏区,不带赃物标签。
- Ponero 兑换处(声望 ≥ 30):任意币兑入 Ponero 时这部分价值的污点留在原地,兑入、兑出各收 8%,每天额度 1000 信用点;Ponero 价 = PSC × 0.5。从 Ponero 兑出的币在出金审查里按"刚出混币器"加重。
- 游戏内验证:声望 75 时上架 5 种;用 CRT 买一把左轮 312 点,余额 3000 → 2688,污点 2000 → 1875(按付款占钱包的比例);5 PSC 兑入得 9.2 Ponero(500 × 0.92 / 50),当天额度剩 500。
- 还没做:订单(托管、送货、死投点)、平台跑路、包裹截获、蜜罐、受控交付。
- 订单:黑市页新增"订单"和"我的订单与情报"。目录每周刷新,4 个卖家(成交数、好评率公开,15% 是骗子,多半是新号,也有刷过好评的老号),价格 = 基础价 × 0.85。托管多付 5%,收货后放款,卖家跑路时退款(污点与成本一并还回,不能借退款洗钱);提前放款再打九折,只有成交 ≥ 30 的卖家接受,骗子拿钱就跑。
- 配送:送货上门 3 天,到货后开店时快递员作为顾客上门,对话结束把货放进储藏区;死投点 2 天,到货后晚上去垃圾场,货进外出背包。上门被截获 8%,死投 3%,死投另有 12% 被别人先挖走;镇压期间到期顺延、截获 +15%。
- 平台跑路:每晚概率 0.2% + 在途款项每万信用点 0.4%(封顶 5%),跑路后在途款项归零、目录与现货清空、入口关闭 10 天。论坛情报:骗子卖家有一半概率被人点名,跑路风险升高时出现提币变慢的帖子。
- 游戏内验证:下单后在途显示在"我的订单";过夜到期,两单到货,一单骗子卖家托管退款(CRT +143,污点 +57);快递员对话走完,货进储藏区,快递员自动离开。没有在游戏里验证:死投点取货(要去垃圾场)、平台跑路与截获的实际触发(只有单元测试)。
- 还没做:蜜罐、受控交付(第三批)。
### 第三阶段:数值调优(进行中)
+1 -1
View File
@@ -18,7 +18,7 @@ namespace Decentralized.Domain
/// 付出去的币连同按比例的污点一起留在卖家那边;Ponero 兑换处把任意币兑成无法追踪的 Ponero(污点留在原地),有每日额度和手续费。
/// 镇压期间整个平台下线。
/// </summary>
public sealed class BlackMarket
public sealed partial class BlackMarket
{
public List<BmOffer> Offers = new List<BmOffer>();
public int RefreshedDay = -999;
+185
View File
@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
public enum PayMode { Escrow, Advance }
public enum Delivery { Door, Drop }
public enum OrderState { Pending, Ready, Done, Refunded, Lost }
/// <summary>黑市上的卖家:成交数与好评率公开;是不是骗子是隐藏的。</summary>
public sealed class BmSeller
{
public string Name;
public int Trades;
public int Rating; // 好评率 %
public bool Scam;
/// <summary>只有成交数够多的卖家才接受提前放款。</summary>
public bool Advance => Trades >= Tuning.BmAdvanceMinTrades;
}
/// <summary>订单目录里的一件货:卖家、货、价格(订单价,已折扣)。</summary>
public sealed class BmListing
{
public int Seller;
public string Item;
public int Level;
public int Price;
}
public sealed class BmOrder
{
public string Item;
public string Seller;
public bool SellerScam;
public PayMode Mode;
public Delivery Delivery;
public int Price;
public string Asset;
public double Qty;
/// <summary>付款时留在卖家那边的污点与成本:退款时一并还回,免得"付脏币再退款"洗钱。</summary>
public double Taint, Basis;
public int PlacedDay, DueDay;
public OrderState State = OrderState.Pending;
/// <summary>终态的原因:done / scam / rug / intercept / theft / refund。</summary>
public string Why;
}
/// <summary>夜间发生的订单事件,供日志与新闻使用。</summary>
public sealed class BmEvent
{
public string Kind; // ready / scam_refund / scam_loss / rug / intercept / theft / reopen
public BmOrder Order;
public double Amount;
}
public sealed partial class BlackMarket
{
public List<BmSeller> Sellers = new List<BmSeller>();
public List<BmListing> Catalog = new List<BmListing>();
public List<BmOrder> Orders = new List<BmOrder>();
public List<string> Forum = new List<string>();
/// <summary>平台跑路后关闭到这一天(含)。</summary>
public int ClosedUntil = -1;
private static readonly string[] NameA = { "rat", "grey", "static", "null", "dust", "iron", "pale", "wire", "salt", "ghost" };
private static readonly string[] NameB = { "king", "mule", "broker", "hand", "cat", "fox", "tooth", "lamp", "nine", "dealer" };
public bool Closed(int day) => day <= ClosedUntil;
/// <summary>托管中和已提前放款但尚未交付的资金(信用点),平台跑路的概率随它增加。</summary>
public double Held => Orders.Where(o => o.State == OrderState.Pending).Sum(o => (double)o.Price);
public static int OrderPrice(long baseValue) => (int)Math.Max(1, Math.Round(baseValue * (1 - Tuning.BmOrderDiscount)));
public static int EscrowFee(int price) => (int)Math.Ceiling(price * Tuning.BmEscrowFee);
/// <summary>下单实付(信用点):托管 = 订单价 + 托管费;提前放款 = 订单价打折。</summary>
public static int Pay(int price, PayMode m) => m == PayMode.Escrow ? price + EscrowFee(price) : (int)Math.Round(price * (1 - Tuning.BmAdvanceDiscount));
public static int Days(Delivery d) => d == Delivery.Door ? Tuning.BmDoorDays : Tuning.BmDropDays;
public double RugChance => Math.Min(Tuning.BmRugMax, Tuning.BmRugBase + Held / 10000.0 * Tuning.BmRugPerTenK);
/// <summary>每周重新生成卖家、目录和论坛情报(与现货同一天刷新)。</summary>
public void RefreshOrders(int rep, Rng rng, Func<string, long> value)
{
Sellers.Clear(); Catalog.Clear(); Forum.Clear();
for (int i = 0; i < Tuning.BmSellerCount; i++)
{
bool scam = rng.Chance(Tuning.BmScamSellerShare);
// 骗子多半是新号,也有刷过好评的老号
bool fresh = scam ? rng.Chance(0.6) : rng.Chance(0.25);
var s = new BmSeller
{
Name = NameA[rng.Range(0, NameA.Length - 1)] + "_" + NameB[rng.Range(0, NameB.Length - 1)] + rng.Range(1, 99),
Scam = scam,
Trades = fresh ? rng.Range(1, 12) : rng.Range(25, 220),
Rating = scam && !fresh ? rng.Range(94, 99) : fresh ? rng.Range(70, 100) : rng.Range(90, 99),
};
Sellers.Add(s);
if (scam && rng.Chance(0.5)) Forum.Add("scam:" + s.Name);
}
var pool = Pool.Where(x => x.Value <= MaxLevel(rep)).ToList();
for (int i = 0; i < Tuning.BmSellerCount * 2 && pool.Count > 0; i++)
{
int k = rng.Range(0, pool.Count - 1);
var pick = pool[k];
pool.RemoveAt(k);
long v = value(pick.Key);
if (v <= 0) continue;
Catalog.Add(new BmListing { Seller = i % Sellers.Count, Item = pick.Key, Level = pick.Value, Price = OrderPrice(v) });
}
if (RugChance > Tuning.BmRugBase * 3) Forum.Add("rug");
}
/// <summary>下单:钱包里的币按实付折算扣除,污点与成本按比例留在卖家那边(记在订单上)。成功返回订单。</summary>
public BmOrder Place(BmListing l, PayMode mode, Delivery del, Wallet w, string asset, int day, Func<string, double> price)
{
if (l == null || asset == Assets.CRC || Closed(day)) return null;
var seller = Sellers[l.Seller];
if (mode == PayMode.Advance && !seller.Advance) return null;
double p = price(asset);
if (p <= 0) return null;
int credits = Pay(l.Price, mode);
double qty = credits / p;
if (w.Avail(asset) + 1e-9 < qty) return null;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, credits / total) : 0;
double taint = w.Taint * share, basis = w.Basis * share;
w.Taint -= taint; w.Basis -= basis;
w.Add(asset, -qty);
var o = new BmOrder
{
Item = l.Item, Seller = seller.Name, SellerScam = seller.Scam, Mode = mode, Delivery = del, Price = credits, Asset = asset, Qty = qty,
Taint = taint, Basis = basis, PlacedDay = day, DueDay = day + Days(del),
};
Orders.Add(o);
return o;
}
private static void Refund(Wallet w, BmOrder o)
{
w.Add(o.Asset, o.Qty);
w.Taint += o.Taint; w.Basis += o.Basis;
}
/// <summary>每晚推进订单:平台跑路、到期的订单(卖家跑路、被截获、死投被挖走)、转入可取状态。镇压期间到期顺延一天。</summary>
public List<BmEvent> Night(int day, bool crackdown, Wallet w, Rng rng)
{
var ev = new List<BmEvent>();
if (ClosedUntil == day) ev.Add(new BmEvent { Kind = "reopen" });
if (!Closed(day) && Orders.Any(o => o.State == OrderState.Pending) && rng.Chance(RugChance))
{
foreach (var o in Orders.Where(o => o.State == OrderState.Pending))
{
o.State = OrderState.Lost; o.Why = "rug";
ev.Add(new BmEvent { Kind = "rug", Order = o, Amount = o.Price });
}
ClosedUntil = day + Tuning.BmRugClosedDays;
Offers.Clear(); Catalog.Clear(); Sellers.Clear();
return ev;
}
foreach (var o in Orders.Where(x => x.State == OrderState.Pending).ToList())
{
if (day < o.DueDay) continue;
if (crackdown) { o.DueDay = day + 1; continue; }
if (o.SellerScam)
{
if (o.Mode == PayMode.Escrow) { Refund(w, o); o.State = OrderState.Refunded; o.Why = "scam"; ev.Add(new BmEvent { Kind = "scam_refund", Order = o, Amount = o.Price }); }
else { o.State = OrderState.Lost; o.Why = "scam"; ev.Add(new BmEvent { Kind = "scam_loss", Order = o, Amount = o.Price }); }
continue;
}
double ic = (o.Delivery == Delivery.Door ? Tuning.BmInterceptDoor : Tuning.BmInterceptDrop) + (crackdown ? Tuning.BmInterceptCrackdown : 0);
if (rng.Chance(ic)) { o.State = OrderState.Lost; o.Why = "intercept"; ev.Add(new BmEvent { Kind = "intercept", Order = o, Amount = o.Price }); continue; }
if (o.Delivery == Delivery.Drop && rng.Chance(Tuning.BmDropTheft)) { o.State = OrderState.Lost; o.Why = "theft"; ev.Add(new BmEvent { Kind = "theft", Order = o, Amount = o.Price }); continue; }
o.State = OrderState.Ready;
ev.Add(new BmEvent { Kind = "ready", Order = o });
}
Orders.RemoveAll(o => o.State != OrderState.Pending && o.State != OrderState.Ready && day - o.DueDay > 14);
return ev;
}
public void Deliver(BmOrder o) { o.State = OrderState.Done; o.Why = "done"; }
public IEnumerable<BmOrder> ReadyFor(Delivery d) => Orders.Where(o => o.State == OrderState.Ready && o.Delivery == d);
}
}
+5
View File
@@ -89,6 +89,11 @@ namespace Decentralized.Domain
/// <summary>Ponero 兑换处:开放所需黑市声望、每次兑入 / 兑出的手续费、每天可兑换的额度(信用点)、对 PSC 的价格比。</summary>
public const int PoneroOpenRep = 30;
public const double PoneroFee = 0.08, PoneroDailyCap = 1000, PoneroPscRatio = 0.5;
/// <summary>黑市订单:订单价相对游戏基础价的折扣、托管费、提前放款再折扣、提前放款所需卖家成交数、上门 / 死投的天数、
/// 上门 / 死投被截获的概率(镇压期间再加)、死投被别人先挖走的概率、骗子卖家占比、每晚平台跑路基础概率与每万信用点托管资金加的概率(封顶)、跑路后关闭的天数。</summary>
public const double BmOrderDiscount = 0.15, BmEscrowFee = 0.05, BmAdvanceDiscount = 0.10, BmInterceptDoor = 0.08, BmInterceptDrop = 0.03, BmInterceptCrackdown = 0.15;
public const double BmDropTheft = 0.12, BmScamSellerShare = 0.15, BmRugBase = 0.002, BmRugPerTenK = 0.004, BmRugMax = 0.05;
public const int BmAdvanceMinTrades = 30, BmDoorDays = 3, BmDropDays = 2, BmRugClosedDays = 10, BmSellerCount = 4;
/// <summary>黑市人脉升级的价格。</summary>
public const int BmUnlockPrice = 1500;
/// <summary>单个金融违规事件记入的罪证上限。</summary>
+142
View File
@@ -0,0 +1,142 @@
using System.Linq;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// Web3 黑市的货物进出:每周刷新现货与订单目录,夜间推进订单并写夜间日志,
/// 上门(快递员当顾客上门)和死投(去垃圾场取货)把到货交给玩家。
/// </summary>
public static class BlackDelivery
{
public static void Subscribe()
{
ModHook.OnGenerateCustomerNormal += OnGenerate;
ModHook.OnAfterhourArriveLate += OnArrive;
}
public static void Unsubscribe()
{
ModHook.OnGenerateCustomerNormal -= OnGenerate;
ModHook.OnAfterhourArriveLate -= OnArrive;
}
public static long Value(string id)
{
var it = ModHelper.SpawnItem(id);
if (it == null) return 0;
long v = it.GetBaseValue();
it.Destroy();
return v;
}
public static string ItemName(string id)
{
var it = ModHelper.SpawnItem(id);
if (it == null) return id;
string n = it.name;
it.Destroy();
return n;
}
/// <summary>每周刷新:现货与订单目录一起。返回是否刷新了。</summary>
public static bool Restock(SaveData d, int day, bool force = false)
{
int rep = StoreReputation.GetBMReputation();
if (d.Black.Closed(day)) return false;
if (force) d.Black.RefreshedDay = -999;
if (!d.Black.Refresh(day, rep, d.Rng, Value)) return false;
d.Black.RefreshOrders(rep, d.Rng, Value);
return true;
}
public static void Night(SaveData d, int day, bool crackdown)
{
var ps = PlayerStore.Instance;
foreach (var e in d.Black.Night(day, crackdown, d.Hot, d.Rng))
{
string name = e.Order != null ? ItemName(e.Order.Item) : null;
Log.Info($"黑市订单事件:{e.Kind} {e.Order?.Item} {e.Order?.Seller} {e.Order?.Mode}/{e.Order?.Delivery} 金额 {e.Amount:0}");
switch (e.Kind)
{
case "ready": ps.AddNightLog(L.T("nightlog.bm.ready." + e.Order.Delivery, name), "99bb66"); break;
case "scam_refund": ps.AddNightLog(L.T("nightlog.bm.scam_refund", e.Order.Seller, name), "cc7755"); break;
case "scam_loss": ps.AddNightLog(L.T("nightlog.bm.scam_loss", e.Order.Seller, name, Fmt.Num(e.Amount)), "cc7755"); break;
case "intercept": ps.AddNightLog(L.T("nightlog.bm.intercept", name, Fmt.Num(e.Amount)), "cc7755"); break;
case "theft": ps.AddNightLog(L.T("nightlog.bm.theft", name, Fmt.Num(e.Amount)), "cc7755"); break;
case "rug": ps.AddNightLog(L.T("nightlog.bm.rug", name, Fmt.Num(e.Amount)), "cc7755"); break;
case "reopen": ps.AddNightLog(L.T("nightlog.bm.reopen"), "99bb66"); break;
}
if (e.Kind == "rug") Night_RugNews(day);
}
if (Unlock.BlackUnlocked && !crackdown) Restock(d, day);
}
private static void Night_RugNews(int day) => News.Show("pd_news_bmrug_" + day, L.T("news.bmrug.title"), L.T("news.bmrug.desc"), 4);
// ---- 上门:开门后有到货的当天,快递员排进顾客队伍 ----
private static int courierDay = -1;
private static void OnGenerate(StoreClientManager manager)
{
if (Session.Data == null || !Unlock.BlackUnlocked) return;
Log.Guard("黑市快递员", () =>
{
int day = StoreStation.GetDayCounter();
if (day == courierDay || !Session.Data.Black.ReadyFor(Delivery.Door).Any()) return;
courierDay = day;
manager.AddClient(CreateCourier());
});
}
public static StoreClient CreateCourier()
{
var rng = Session.Data.Rng;
var c = new StoreClient
{
identifier = "pd_bm_courier",
displayName = L.T("courier.name"),
spriteName = SpriteDict.GetRandomScavSpriteName(),
clientIntent = StoreClient.ClientIntent.SELL,
};
StoreClientFactionSetup.InitScav(c);
c.mainDialogue.SetText(c.displayName, L.T("courier.line1." + rng.Range(1, 2))).NextDialogue().SetText(c.displayName, L.T("courier.line2." + rng.Range(1, 2)))
.SetEndAction(() => Log.Guard("黑市快递交货", () => { DeliverDoor(); PlayerStore.Instance.DismissCurrentClient(); }));
c.AddBasicDialog();
c.CompleteClientCreation();
return c;
}
private static void DeliverDoor()
{
var d = Session.Data;
foreach (var o in d.Black.ReadyFor(Delivery.Door).ToList())
{
Items.Give(ModHelper.SpawnItem(o.Item));
d.Black.Deliver(o);
Log.Info($"黑市快递:{o.Item} 已放进储藏区");
}
Session.Save();
}
// ---- 死投:晚上去垃圾场,货在那里 ----
private static void OnArrive(string locationId)
{
if (locationId != "dumping_ground" || Session.Data == null || !Unlock.BlackUnlocked) return;
Log.Guard("黑市死投", () =>
{
var d = Session.Data;
var list = d.Black.ReadyFor(Delivery.Drop).ToList();
foreach (var o in list)
{
var item = ModHelper.SpawnItem(o.Item);
GeneralHelper.SetItemOwned(item, true);
GraphUtils.EmporiumTryAdd(EmporiumEntry.Instance.afterhourInventory, item);
d.Black.Deliver(o);
Log.Info($"黑市死投:{o.Item} 已放进外出背包");
}
if (list.Count > 0) { Session.Save(); }
});
}
}
}
+9 -9
View File
@@ -51,20 +51,20 @@ namespace Decentralized
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market +50 rep, restock (test)", () => Log.Guard("黑市(测试)", () =>
{
StoreReputation.ModReputation("FACTION_BLACK_MARKET", 50);
if (!Unlock.IsUnlocked) ModHelper.GetNetworkUpgrade(Unlock.Id)?.Unlock();
if (!Unlock.BlackUnlocked) ModHelper.GetNetworkUpgrade(Unlock.BlackId)?.Unlock();
var d = Session.Data;
d.Black.RefreshedDay = -999;
d.Hot.Add(Assets.PSC, 20); d.Hot.Add(Assets.CRT, 3000); d.Hot.Taint += 2000; d.Hot.Basis += 1000;
d.Black.Refresh(StoreStation.GetDayCounter(), StoreReputation.GetBMReputation(), d.Rng, id =>
{
var it = ModHelper.SpawnItem(id);
if (it == null) return 0;
long v = it.GetBaseValue();
it.Destroy();
return v;
});
BlackDelivery.Restock(d, StoreStation.GetDayCounter(), true);
Log.Info($"黑市:声望 {StoreReputation.GetBMReputation()},上架 {d.Black.Offers.Count} 种:{string.Join(", ", d.Black.Offers.Select(o => o.Item + "×" + o.Qty + "@" + o.Price))},解锁 {Unlock.BlackUnlocked}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Courier (test)", () => Log.Guard("黑市快递(测试)", () =>
{
foreach (var o in Session.Data.Black.Orders.Where(x => x.State == OrderState.Pending)) { o.State = OrderState.Ready; }
PlayerStore.Instance.currentClientInstance = null;
PlayerStore.Instance.storeClientManager.clientStack.Insert(0, BlackDelivery.CreateCourier());
PlayerStore.Instance.TryCallNextClient();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer (test)", () => Log.Guard("用币付款顾客(测试)", () =>
{
var c = new StoreClient
+2
View File
@@ -17,6 +17,7 @@ namespace Decentralized
DealerStock.Subscribe();
UsedDealerClient.Subscribe();
CoinCustomers.Subscribe();
BlackDelivery.Subscribe();
GpuInspectView.Ensure();
ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame;
@@ -30,6 +31,7 @@ namespace Decentralized
DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe();
CoinCustomers.Unsubscribe();
BlackDelivery.Unsubscribe();
GpuInspectView.Remove();
ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame;
@@ -230,3 +230,50 @@ black.desk.in,Into Ponero
black.desk.out,Out of Ponero
black.desk.go,Swap
black.desk.fail,Swap failed: balance, limit or amount is wrong.
black.sub.orders,Orders
black.sub.mine,My orders & intel
black.rugged,"The platform is down; nobody will reopen for {0} more days."
black.seller,"Seller {0} trades {1} rating {2}%"
black.orders.h,Order catalog
black.listing,"{0} pay {1} credits"
black.order,Order
black.noadvance,This seller does not take advance release
black.orders.hint,"Cheaper than spot, but it takes days|and things can go wrong.|Escrow: {0}% fee, released on delivery;|refunded if the seller runs.|Advance: {1}% off, but a scammer keeps it;|only sellers with >= {2} trades accept.|The platform can rug too; escrow and|advance money both sit with it."
black.mode.Escrow,Escrow
black.mode.Advance,Advance release
black.del.Door,Door delivery
black.del.Drop,Dead drop
black.del.hint.Door,"A courier comes by in {0} days (as a customer when you open); higher interception chance."
black.del.hint.Drop,"Left at the dumping ground in {0} days, pick it up at night; low interception chance, but someone may dig it up first."
black.ordered,"Ordered: {0}, due day {1}."
black.mine.h,My orders
black.mine.none,No orders yet.
black.mine.line,"{0} ({1}, {2}): {3}, day {4}{5}"
black.mine.drop,", pick up at the dumping ground"
black.state.Pending,in transit
black.state.Ready,arrived
black.state.Done,picked up
black.state.Refunded.scam,seller ran, refunded
black.state.Lost.scam,seller ran, money gone
black.state.Lost.rug,platform rugged, money gone
black.state.Lost.intercept,intercepted
black.state.Lost.theft,dug up by someone else
black.forum.h,Forum intel
black.forum.quiet,Quiet lately.
black.forum.scam,"Someone says {0} takes the money and never ships."
black.forum.rug,"Withdrawals have been slow lately, the site is running promos, and a lot of complaint threads got deleted."
nightlog.bm.ready.Door,"Black market: {0} arrived; a courier will come by once you open."
nightlog.bm.ready.Drop,"Black market: {0} is at the dumping ground, go get it at night."
nightlog.bm.scam_refund,"Black market: seller {0} ran; the escrow for {1} was refunded."
nightlog.bm.scam_loss,"Black market: seller {0} took the advance and ran; {1} is gone ({2} credits)."
nightlog.bm.intercept,"Black market: the parcel with {0} was intercepted; {1} credits of payment lost."
nightlog.bm.theft,"Black market: {0} at the dumping ground was dug up by someone else; {1} credits lost."
nightlog.bm.rug,"Black market: the platform rugged; the in-transit funds for {0} ({1}) are gone."
nightlog.bm.reopen,Black market: someone reopened at the old spot.
news.bmrug.title,Black-market platform rugged
news.bmrug.desc,"The black market's on-chain entrance went dark overnight, wiping out all escrow and advance funds. A new entrance will appear in a few days."
courier.name,Courier
courier.line1.1,"Delivery's here."
courier.line1.2,"Package for you."
courier.line2.1,"Don't tell me what's inside. I don't want to know."
courier.line2.2,"Dropped it here. I'm gone."
1 # Probably Decentralized localization table: English (en)
230 black.desk.out
231 black.desk.go
232 black.desk.fail
233 black.sub.orders
234 black.sub.mine
235 black.rugged
236 black.seller
237 black.orders.h
238 black.listing
239 black.order
240 black.noadvance
241 black.orders.hint
242 black.mode.Escrow
243 black.mode.Advance
244 black.del.Door
245 black.del.Drop
246 black.del.hint.Door
247 black.del.hint.Drop
248 black.ordered
249 black.mine.h
250 black.mine.none
251 black.mine.line
252 black.mine.drop
253 black.state.Pending
254 black.state.Ready
255 black.state.Done
256 black.state.Refunded.scam
257 black.state.Lost.scam
258 black.state.Lost.rug
259 black.state.Lost.intercept
260 black.state.Lost.theft
261 black.forum.h
262 black.forum.quiet
263 black.forum.scam
264 black.forum.rug
265 nightlog.bm.ready.Door
266 nightlog.bm.ready.Drop
267 nightlog.bm.scam_refund
268 nightlog.bm.scam_loss
269 nightlog.bm.intercept
270 nightlog.bm.theft
271 nightlog.bm.rug
272 nightlog.bm.reopen
273 news.bmrug.title
274 news.bmrug.desc
275 courier.name
276 courier.line1.1
277 courier.line1.2
278 courier.line2.1
279 courier.line2.2
@@ -229,3 +229,50 @@ black.desk.in,兑入 Ponero
black.desk.out,兑出 Ponero
black.desk.go,兑换
black.desk.fail,兑换失败:余额、额度或数量不对。
black.sub.orders,订单
black.sub.mine,我的订单与情报
black.rugged,"平台出事了,入口关闭,还要 {0} 天才会有人重新开张。"
black.seller,"卖家 {0} 成交 {1} 好评 {2}%"
black.orders.h,订单目录
black.listing,"{0} 实付 {1} 信用点等值"
black.order,下单
black.noadvance,这个卖家不接受提前放款
black.orders.hint,"比现货便宜,但要等几天,|路上可能出事。|托管:多付 {0}% 托管费,|收货后放款,卖家跑路时退款。|提前放款:再打 {1}% 折,|卖家若是骗子钱就没了;|成交 ≥ {2} 的卖家才接受。|平台自己也可能跑路,|托管和提前放款的钱都在它手里。"
black.mode.Escrow,托管
black.mode.Advance,提前放款
black.del.Door,"送货上门"
black.del.Drop,死投点
black.del.hint.Door,"{0} 天后快递员上门(开店时来个顾客),被截获的概率较高。"
black.del.hint.Drop,"{0} 天后货放在垃圾场,晚上去取;被截获的概率低,但可能被别人先挖走。"
black.ordered,"已下单:{0},预计第 {1} 天到。"
black.mine.h,我的订单
black.mine.none,还没有订单。
black.mine.line,"{0}({1},{2}):{3},第 {4} 天{5}"
black.mine.drop,,去垃圾场取
black.state.Pending,在途
black.state.Ready,到货
black.state.Done,已取
black.state.Refunded.scam,卖家跑路,已退款
black.state.Lost.scam,卖家跑路,钱没了
black.state.Lost.rug,平台跑路,钱没了
black.state.Lost.intercept,被截获
black.state.Lost.theft,被别人先挖走
black.forum.h,论坛情报
black.forum.quiet,最近很安静。
black.forum.scam,"有人说 {0} 收了钱不发货。"
black.forum.rug,"最近提币变慢了,全场还在搞促销,投诉帖被删了不少。"
nightlog.bm.ready.Door,"黑市:{0} 到货,开店后会有快递员上门。"
nightlog.bm.ready.Drop,"黑市:{0} 已放在垃圾场,晚上去取。"
nightlog.bm.scam_refund,"黑市:卖家 {0} 跑路了,{1} 的托管款已退回。"
nightlog.bm.scam_loss,"黑市:卖家 {0} 拿了提前放款的钱跑路,{1} 没有了({2} 信用点等值)。"
nightlog.bm.intercept,"黑市:{0} 的包裹被截获,{1} 信用点等值的货款没了。"
nightlog.bm.theft,"黑市:垃圾场的 {0} 被别人先挖走了,{1} 信用点等值的货款没了。"
nightlog.bm.rug,"黑市:平台跑路,{0} 的在途款项({1})归零。"
nightlog.bm.reopen,黑市:有人在老地方重新开张了。
news.bmrug.title,黑市平台跑路
news.bmrug.desc,"黑市的链上入口一夜之间下线,托管和提前放款的资金全部清零。几天后会有新的入口。"
courier.name,快递员
courier.line1.1,"东西到了。"
courier.line1.2,"有你的包裹。"
courier.line2.1,"别问我里面是什么,我也不想知道。"
courier.line2.2,"货放这儿了,我走了。"
1 # Probably Decentralized 本地化表:简体中文(zh)
229 black.desk.out
230 black.desk.go
231 black.desk.fail
232 black.sub.orders
233 black.sub.mine
234 black.rugged
235 black.seller
236 black.orders.h
237 black.listing
238 black.order
239 black.noadvance
240 black.orders.hint
241 black.mode.Escrow
242 black.mode.Advance
243 black.del.Door
244 black.del.Drop
245 black.del.hint.Door
246 black.del.hint.Drop
247 black.ordered
248 black.mine.h
249 black.mine.none
250 black.mine.line
251 black.mine.drop
252 black.state.Pending
253 black.state.Ready
254 black.state.Done
255 black.state.Refunded.scam
256 black.state.Lost.scam
257 black.state.Lost.rug
258 black.state.Lost.intercept
259 black.state.Lost.theft
260 black.forum.h
261 black.forum.quiet
262 black.forum.scam
263 black.forum.rug
264 nightlog.bm.ready.Door
265 nightlog.bm.ready.Drop
266 nightlog.bm.scam_refund
267 nightlog.bm.scam_loss
268 nightlog.bm.intercept
269 nightlog.bm.theft
270 nightlog.bm.rug
271 nightlog.bm.reopen
272 news.bmrug.title
273 news.bmrug.desc
274 courier.name
275 courier.line1.1
276 courier.line1.2
277 courier.line2.1
278 courier.line2.2
+1 -9
View File
@@ -101,15 +101,7 @@ namespace Decentralized
foreach (var t in d.Hot.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 && !events.Contains(MarketEvent.Crackdown))
d.Black.Refresh(day, StoreReputation.GetBMReputation(), d.Rng, id =>
{
var it = ModHelper.SpawnItem(id);
if (it == null) return 0;
long v = it.GetBaseValue();
it.Destroy();
return v;
});
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();
+97 -15
View File
@@ -264,7 +264,10 @@ namespace Decentralized
});
}
private static bool _blackDesk;
private enum BlackTab { Shop, Orders, Mine, Desk }
private static BlackTab _blackTab = BlackTab.Shop;
private static PayMode _mode = PayMode.Escrow;
private static Delivery _delivery = Delivery.Door;
private static string _deskAsset = Assets.PSC;
private static bool _deskIn = true;
private static string _payAsset = Assets.CRT;
@@ -278,14 +281,19 @@ namespace Decentralized
if (d.ActiveEvents.Contains(MarketEvent.Crackdown)) { b.AddLabel(L.T("black.offline")); return; }
if (!BlackMarket.Open(rep)) { b.AddLabel(L.T("black.closed", Tuning.BmOpenRep, rep)); return; }
bool desk = BlackMarket.DeskOpen(rep);
if (desk)
b.BeginRow();
foreach (BlackTab t in Enum.GetValues(typeof(BlackTab)))
{
b.BeginRow();
b.AddButton(_blackDesk ? L.T("black.sub.shop") : $"[{L.T("black.sub.shop")}]", () => { _blackDesk = false; _message = null; Open(); });
b.AddButton(_blackDesk ? $"[{L.T("black.sub.desk")}]" : L.T("black.sub.desk"), () => { _blackDesk = true; _message = null; Open(); });
b.End();
if (t == BlackTab.Desk && !desk) continue;
var tab = t;
string name = L.T("black.sub." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _blackTab ? $"[{name}]" : name, () => { _blackTab = tab; _message = null; Open(); });
}
if (desk && _blackDesk) { DeskPage(b, rep, day); return; }
b.End();
if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1)); return; }
if (_blackTab == BlackTab.Desk && desk) { DeskPage(b, rep, day); return; }
if (_blackTab == BlackTab.Orders) { OrdersPage(b, day); return; }
if (_blackTab == BlackTab.Mine) { MinePage(b, day); return; }
var w = d.Hot;
Columns(b, l =>
{
@@ -314,14 +322,7 @@ namespace Decentralized
});
}
private static string ItemName(string id)
{
var it = ModHelper.SpawnItem(id);
if (it == null) return id;
string n = it.name;
it.Destroy();
return n;
}
private static string ItemName(string id) => BlackDelivery.ItemName(id);
private static void BuyOffer(BmOffer o)
{
@@ -333,6 +334,87 @@ namespace Decentralized
Open();
}
private static string SellerText(BmSeller s) => L.T("black.seller", s.Name, s.Trades, s.Rating);
/// <summary>订单目录:比现货便宜,要等几天,可能被截获、被骗。</summary>
private static void OrdersPage(CustomUIBuilder b, int day)
{
var d = Session.Data;
var w = d.Hot;
Columns(b, l =>
{
l.AddHeader(L.T("black.orders.h"));
if (d.Black.Catalog.Count == 0) l.AddLabel(L.T("black.empty"));
foreach (var x in d.Black.Catalog.ToList())
{
var listing = x;
var seller = d.Black.Sellers[listing.Seller];
l.AddLabel(L.T("black.listing", ItemName(listing.Item), BlackMarket.Pay(listing.Price, _mode)));
l.AddLabel(" " + SellerText(seller));
bool ok = _mode == PayMode.Escrow || seller.Advance;
l.AddButton(ok ? L.T("black.order") : L.T("black.noadvance"), () => { if (ok) PlaceOrder(listing); });
}
}, r =>
{
r.AddHeader(L.T("black.pay.h"));
foreach (var line in L.T("black.orders.hint", (int)Math.Round(Tuning.BmEscrowFee * 100), (int)Math.Round(Tuning.BmAdvanceDiscount * 100), Tuning.BmAdvanceMinTrades).Split('|')) r.AddLabel(line);
r.BeginRow();
foreach (PayMode m in Enum.GetValues(typeof(PayMode)))
{
var mode = m;
string n = L.T("black.mode." + m);
r.AddButton(mode == _mode ? $"[{n}]" : n, () => { _mode = mode; Open(); });
}
r.End();
r.BeginRow();
foreach (Delivery dl in Enum.GetValues(typeof(Delivery)))
{
var del = dl;
string n = L.T("black.del." + dl, Tuning.BmDoorDays);
r.AddButton(del == _delivery ? $"[{n}]" : n, () => { _delivery = del; Open(); });
}
r.End();
r.AddLabel(L.T("black.del.hint." + _delivery, BlackMarket.Days(_delivery)));
r.BeginRow();
foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero })
{
var asset = a;
r.AddButton(asset == _payAsset ? $"[{asset}]" : asset, () => { _payAsset = asset; Open(); });
}
r.End();
r.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(w.Avail(_payAsset))));
});
}
private static void PlaceOrder(BmListing l)
{
var d = Session.Data;
var o = d.Black.Place(l, _mode, _delivery, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price);
if (o == null) { _message = L.T("msg.balance"); Open(); return; }
_message = L.T("black.ordered", ItemName(o.Item), o.DueDay);
Session.Save();
Open();
}
/// <summary>我的订单与论坛情报。</summary>
private static void MinePage(CustomUIBuilder b, int day)
{
var d = Session.Data;
Columns(b, l =>
{
l.AddHeader(L.T("black.mine.h"));
if (d.Black.Orders.Count == 0) l.AddLabel(L.T("black.mine.none"));
foreach (var o in d.Black.Orders.OrderByDescending(x => x.PlacedDay))
l.AddLabel(L.T("black.mine.line", ItemName(o.Item), o.Seller, L.T("black.mode." + o.Mode), L.T("black.state." + o.State + (o.Why != null && o.State != OrderState.Done ? "." + o.Why : "")), o.DueDay, o.Delivery == Delivery.Drop && o.State == OrderState.Ready ? L.T("black.mine.drop") : ""));
}, r =>
{
r.AddHeader(L.T("black.forum.h"));
if (d.Black.Forum.Count == 0) r.AddLabel(L.T("black.forum.quiet"));
foreach (var f in d.Black.Forum)
r.AddLabel(f.StartsWith("scam:") ? L.T("black.forum.scam", f.Substring(5)) : L.T("black.forum." + f));
});
}
private static void DeskPage(CustomUIBuilder b, int rep, int day)
{
var d = Session.Data;
@@ -9,6 +9,8 @@ internal static class Program
{
private static int _failed, _passed;
private static double BlackMarketTestRug() => Tuning.BmRugBase * 3;
private static void Check(bool ok, string what)
{
if (ok) _passed++;
@@ -583,6 +585,55 @@ internal static class Program
Check(Near(back, 10 * 50 * (1 - Tuning.PoneroFee), 1e-9) && Near(w.Get(Assets.CRT), back, 1e-9), "兑出 Ponero");
});
Test("黑市订单", () =>
{
Func<string, double> price = a => a == Assets.CRT ? 1 : 100;
Func<string, long> value = id => 200;
var bm = new BlackMarket();
bm.RefreshOrders(75, new Rng(5), value);
Check(bm.Sellers.Count == Tuning.BmSellerCount && bm.Catalog.Count > 0 && bm.Catalog.All(l => l.Price == 170), "目录:卖家与订单价(基础价 × 0.85)");
Check(BlackMarket.Pay(170, PayMode.Escrow) == 170 + 9 && BlackMarket.Pay(170, PayMode.Advance) == 153, "托管多付 5%,提前放款打九折");
var good = new BmSeller { Name = "g", Trades = 100, Rating = 95 };
var bad = new BmSeller { Name = "b", Trades = 100, Rating = 95, Scam = true };
bm.Sellers.Clear(); bm.Sellers.Add(good); bm.Sellers.Add(bad);
var lg = new BmListing { Seller = 0, Item = "x", Price = 1000 };
var lb = new BmListing { Seller = 1, Item = "x", Price = 1000 };
var w = new Wallet { Address = "t" };
w.Add(Assets.CRT, 10000); w.Taint = 4000; w.Basis = 5000;
var o = bm.Place(lb, PayMode.Escrow, Delivery.Door, w, Assets.CRT, 10, price);
Check(o != null && Near(w.Get(Assets.CRT), 10000 - 1050, 1e-9) && w.Taint < 4000 && bm.Place(lg, PayMode.Escrow, Delivery.Door, w, Assets.CRC, 10, price) == null, "托管下单:扣币、污点留在订单上;不收 CRC");
double tBefore = 4000;
var ev = bm.Night(13, false, w, new Rng(1));
Check(ev.Any(e => e.Kind == "scam_refund") && Near(w.Get(Assets.CRT), 10000, 1e-6) && Near(w.Taint, tBefore, 1e-6) && Near(w.Basis, 5000, 1e-6), "骗子卖家 + 托管:退款,污点与成本一并还回(不能借退款洗钱)");
var o2 = bm.Place(lb, PayMode.Advance, Delivery.Drop, w, Assets.CRT, 20, price);
var ev2 = bm.Night(22, false, w, new Rng(2));
Check(o2.State == OrderState.Lost && Near(w.Get(Assets.CRT), 10000 - 900, 1e-6), "骗子卖家 + 提前放款:钱没了");
var o3 = bm.Place(lg, PayMode.Escrow, Delivery.Door, w, Assets.CRT, 30, price);
Check(bm.Night(31, false, w, new Rng(3)).Count == 0 && o3.State == OrderState.Pending, "没到期不动");
int ready = 0, lost = 0;
for (int i = 0; i < 400; i++)
{
var t = new BlackMarket(); t.Sellers.Add(good);
var ww = new Wallet(); ww.Add(Assets.CRT, 5000);
var oo = t.Place(new BmListing { Seller = 0, Item = "x", Price = 100 }, PayMode.Escrow, Delivery.Door, ww, Assets.CRT, 1, price);
t.Night(oo.DueDay, false, ww, new Rng((ulong)i + 7));
if (oo.State == OrderState.Ready) ready++; else lost++;
}
Check(ready > 340 && lost > 10, $"上门:约 {Tuning.BmInterceptDoor:P0} 被截获(跑路另算):到货 {ready},丢失 {lost}");
var c = new BlackMarket(); c.Sellers.Add(good);
var cw = new Wallet(); cw.Add(Assets.CRT, 5000);
var co = c.Place(new BmListing { Seller = 0, Item = "x", Price = 100 }, PayMode.Escrow, Delivery.Door, cw, Assets.CRT, 1, price);
c.Night(co.DueDay, true, cw, new Rng(1));
Check(co.State == OrderState.Pending && co.DueDay == 5, "镇压期间到期顺延一天");
// 跑路:托管资金多则概率高;跑路后关闭
var r = new BlackMarket(); r.Sellers.Add(good);
var rw = new Wallet(); rw.Add(Assets.CRT, 1000000);
for (int i = 0; i < 20; i++) r.Place(new BmListing { Seller = 0, Item = "x", Price = 10000 }, PayMode.Escrow, Delivery.Door, rw, Assets.CRT, 1, price);
Check(r.RugChance > BlackMarketTestRug(), "托管资金越多越容易跑路");
r.ClosedUntil = 40;
Check(r.Closed(40) && !r.Closed(41) && r.Place(new BmListing { Seller = 0, Item = "x", Price = 1 }, PayMode.Escrow, Delivery.Door, rw, Assets.CRT, 30, price) == null, "关闭期间不能下单");
});
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
return _failed == 0 ? 0 : 1;
}