Mod: mining rig, graphics cards, trading terminal, nightly settlement
First playable slice, tested in game on the test save: - Network upgrade "Mining Access" (300) delivers a trading terminal, a mining rig and an RZX 50. - The rig is a machine with a battery slot and four card slots; each night it draws its total power from the battery and its cards mine PSCoin. Cards show hashrate, power draw and condition, wear every night and can burn out. All state lives on item tags or in the mod save data. - Trading terminal with wallet, trade (four pairs, deposit credits as CRC, withdraw CRC through the compliance desk) and report pages. - Nightly log lines; exchange opening and halving news on the paper. - English and Chinese text; sprites generated by art/sprites.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
efc58888f3
commit
750b26e4b3
21 files changed
+1040
No files matched your search
@@ -4,3 +4,5 @@
|
|||||||
|
|
||||||
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
|
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
|
||||||
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
|
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
|
||||||
|
- mod 本体(第一阶段的第一部分):人脉网络"矿业渠道"(300)交付交易终端、矿架和一张 RZX 50;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSCoin;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSCoin 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。
|
||||||
|
- 物品图:art/sprites.py 生成显卡(Probidia、PMD、烧毁)、矿架、交易终端。
|
||||||
@@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BC589259
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Decentralized.Domain.Tests", "tests\Decentralized.Domain.Tests\Decentralized.Domain.Tests.csproj", "{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Decentralized.Domain.Tests", "tests\Decentralized.Domain.Tests\Decentralized.Domain.Tests.csproj", "{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Decentralized", "src\Decentralized\Decentralized.csproj", "{6174F3DC-7AB4-4302-9CBB-419B120533B7}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -28,9 +30,14 @@ Global
|
|||||||
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6174F3DC-7AB4-4302-9CBB-419B120533B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6174F3DC-7AB4-4302-9CBB-419B120533B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6174F3DC-7AB4-4302-9CBB-419B120533B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6174F3DC-7AB4-4302-9CBB-419B120533B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{9F515839-7902-4AB8-86D6-33755009DDDF} = {65D2EE76-8B79-48B0-BFF3-BF1B7FE5C096}
|
{9F515839-7902-4AB8-86D6-33755009DDDF} = {65D2EE76-8B79-48B0-BFF3-BF1B7FE5C096}
|
||||||
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5} = {BC589259-3F05-4F25-81C9-183268862282}
|
{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5} = {BC589259-3F05-4F25-81C9-183268862282}
|
||||||
|
{6174F3DC-7AB4-4302-9CBB-419B120533B7} = {65D2EE76-8B79-48B0-BFF3-BF1B7FE5C096}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
@@ -9,7 +9,9 @@ All coins, exchanges and events are fictional and have no real-world value.
|
|||||||
| Path | Content |
|
| Path | Content |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/Decentralized.Domain` | Pure logic (assets and ledger, AMM pairs, market, mining). No game or Unity references. |
|
| `src/Decentralized.Domain` | Pure logic (assets and ledger, AMM pairs, market, mining). No game or Unity references. |
|
||||||
|
| `src/Decentralized` | The mod: items (trading terminal, mining rig, graphics cards), nightly settlement, terminal window, news. Builds into `<game>/Mods/ProbablyDecentralized` (`dotnet build src/Decentralized -p:GameDir=...`). |
|
||||||
| `tests/Decentralized.Domain.Tests` | Console test runner: `dotnet run --project tests/Decentralized.Domain.Tests` |
|
| `tests/Decentralized.Domain.Tests` | Console test runner: `dotnet run --project tests/Decentralized.Domain.Tests` |
|
||||||
|
| `art/sprites.py` | Generates the item sprites into `src/Decentralized/ModFiles/Textures/Items`. |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""物品像素图(游戏物品为 100 PPU,一格 16 像素;形状由不透明像素决定)。
|
||||||
|
|
||||||
|
用法:python3 art/sprites.py → 写出 src/Decentralized/ModFiles/Textures/Items/*.png
|
||||||
|
python3 art/sprites.py --preview DIR → 另存放大 6 倍、桌面底色的预览
|
||||||
|
|
||||||
|
画法沿用 Probably Automated 对讲机图:轮廓统一用 #292234,光从左上来,颜色取自游戏物品图集的调色板。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
C = {
|
||||||
|
'O': (0x29, 0x22, 0x34), # 轮廓
|
||||||
|
'k': (0x37, 0x35, 0x41), # 最暗
|
||||||
|
'd': (0x44, 0x46, 0x4C), # 暗面
|
||||||
|
'm': (0x5F, 0x68, 0x68), # 中间
|
||||||
|
'b': (0x88, 0x94, 0x8A), # 亮面
|
||||||
|
'l': (0xA8, 0xAE, 0xA5), # 受光
|
||||||
|
'h': (0xC4, 0xC4, 0xB0), # 亮边
|
||||||
|
'g': (0x53, 0x58, 0x5B), # 散热片
|
||||||
|
's': (0x27, 0x56, 0x54), # 屏幕底
|
||||||
|
'S': (0x67, 0xAA, 0xA5), # 屏幕字
|
||||||
|
'T': (0x8A, 0xBF, 0xBB), # 屏幕高光
|
||||||
|
'G': (0x5E, 0x9C, 0x4E), # Probidia 绿
|
||||||
|
'g2': (0x3E, 0x6B, 0x36),
|
||||||
|
'R': (0xB0, 0x4A, 0x3E), # PMD 红
|
||||||
|
'r2': (0x7A, 0x33, 0x2E),
|
||||||
|
'Y': (0xC8, 0xA4, 0x4A), # 金手指
|
||||||
|
'c': (0x2E, 0x2A, 0x2A), # 焦黑
|
||||||
|
'e': (0x6B, 0x4A, 0x3A), # 焦痕
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Canvas:
|
||||||
|
def __init__(self, w, h):
|
||||||
|
self.w, self.h = w, h
|
||||||
|
self.px = [[None] * w for _ in range(h)]
|
||||||
|
|
||||||
|
def put(self, x, y, c):
|
||||||
|
if 0 <= x < self.w and 0 <= y < self.h:
|
||||||
|
self.px[y][x] = c
|
||||||
|
|
||||||
|
def rect(self, x0, y0, x1, y1, c):
|
||||||
|
for y in range(y0, y1 + 1):
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
self.put(x, y, c)
|
||||||
|
|
||||||
|
def box(self, x0, y0, x1, y1, fill, outline='O'):
|
||||||
|
self.rect(x0, y0, x1, y1, fill)
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
self.put(x, y0, outline); self.put(x, y1, outline)
|
||||||
|
for y in range(y0, y1 + 1):
|
||||||
|
self.put(x0, y, outline); self.put(x1, y, outline)
|
||||||
|
|
||||||
|
def png(self, path, scale=1, bg=None):
|
||||||
|
raw = bytearray()
|
||||||
|
for row in self.px:
|
||||||
|
line = bytearray()
|
||||||
|
for c in row:
|
||||||
|
r, g, b, a = (0, 0, 0, 0) if c is None else C[c] + (255,)
|
||||||
|
if bg is not None:
|
||||||
|
r, g, b = (round(v * a / 255 + w * (255 - a) / 255) for v, w in zip((r, g, b), bg))
|
||||||
|
a = 255
|
||||||
|
line += bytes((r, g, b, a)) * scale
|
||||||
|
for _ in range(scale):
|
||||||
|
raw.append(0); raw += line
|
||||||
|
|
||||||
|
def chunk(tag, data):
|
||||||
|
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||||
|
|
||||||
|
data = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', self.w * scale, self.h * scale, 8, 6, 0, 0, 0)) \
|
||||||
|
+ chunk(b'IDAT', zlib.compress(bytes(raw), 9)) + chunk(b'IEND', b'')
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def gpu(accent, accent_dark, burnt=False):
|
||||||
|
"""显卡(32×16,2×1 格):外壳、两个风扇、品牌色条、金手指。"""
|
||||||
|
c = Canvas(32, 16)
|
||||||
|
c.box(1, 2, 30, 12, 'm')
|
||||||
|
c.rect(2, 3, 29, 3, 'l') # 顶边受光
|
||||||
|
c.rect(2, 11, 29, 11, 'd') # 底边暗面
|
||||||
|
for cx in (9, 21): # 风扇
|
||||||
|
for y in range(5, 11):
|
||||||
|
for x in range(cx - 4, cx + 5):
|
||||||
|
dx, dy = x - cx, y - 7.5
|
||||||
|
if dx * dx + dy * dy <= 16:
|
||||||
|
c.put(x, y, 'k')
|
||||||
|
if 6 <= dx * dx + dy * dy <= 16:
|
||||||
|
c.put(x, y, 'g')
|
||||||
|
c.put(cx, 7, 'b'); c.put(cx, 8, 'b')
|
||||||
|
c.rect(2, 4, 29, 4, accent) # 品牌色条
|
||||||
|
c.put(2, 5, accent_dark); c.put(29, 5, accent_dark)
|
||||||
|
c.rect(6, 13, 25, 13, 'Y') # 金手指
|
||||||
|
for x in range(6, 26, 2):
|
||||||
|
c.put(x, 14, 'Y')
|
||||||
|
c.rect(30, 3, 30, 11, 'k') # 挡板
|
||||||
|
if burnt:
|
||||||
|
for x, y in [(7, 5), (8, 6), (10, 9), (19, 6), (20, 7), (22, 9), (14, 8), (15, 7), (24, 5)]:
|
||||||
|
c.put(x, y, 'c')
|
||||||
|
c.rect(2, 4, 29, 4, 'e')
|
||||||
|
for x, y in [(12, 3), (13, 3), (17, 11), (26, 10)]:
|
||||||
|
c.put(x, y, 'c')
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def rig():
|
||||||
|
"""矿架(48×48,3×3 格):开放式机架,四层显卡位,左下电源。"""
|
||||||
|
c = Canvas(48, 48)
|
||||||
|
c.box(2, 3, 45, 44, None)
|
||||||
|
for y in range(4, 44):
|
||||||
|
c.put(3, y, 'b'); c.put(44, y, 'd')
|
||||||
|
for x in range(3, 45):
|
||||||
|
c.put(x, 4, 'l'); c.put(x, 43, 'k')
|
||||||
|
for i, y in enumerate((8, 16, 24, 32)): # 四层卡位
|
||||||
|
c.rect(6, y, 41, y + 5, 'k')
|
||||||
|
c.rect(6, y, 41, y, 'g')
|
||||||
|
for x in range(8, 40, 3):
|
||||||
|
c.put(x, y + 2, 'g'); c.put(x, y + 3, 'g')
|
||||||
|
c.put(40, y + 2, 'G' if i % 2 == 0 else 'R')
|
||||||
|
c.box(6, 38, 17, 42, 'd') # 电源
|
||||||
|
c.put(15, 40, 'S')
|
||||||
|
for x in range(20, 42, 2): # 底部走线
|
||||||
|
c.put(x, 40, 'm')
|
||||||
|
c.rect(2, 45, 45, 46, 'k') # 脚
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def terminal():
|
||||||
|
"""交易终端(32×32,2×2 格):小型显示器 + 键盘,屏幕上是 K 线。"""
|
||||||
|
c = Canvas(32, 32)
|
||||||
|
c.box(3, 2, 28, 21, 'm')
|
||||||
|
c.rect(4, 3, 27, 3, 'l')
|
||||||
|
c.box(5, 4, 26, 18, 's')
|
||||||
|
candles = [(8, 12, 15), (11, 10, 14), (14, 11, 13), (17, 7, 12), (20, 8, 11), (23, 5, 10)]
|
||||||
|
for x, top, bot in candles:
|
||||||
|
c.rect(x, top, x, bot, 'S')
|
||||||
|
c.put(x, top - 1, 'T')
|
||||||
|
c.rect(13, 22, 18, 23, 'd') # 支架
|
||||||
|
c.box(2, 24, 29, 30, 'b') # 键盘
|
||||||
|
for y in (26, 28):
|
||||||
|
for x in range(4, 28, 2):
|
||||||
|
c.put(x, y, 'm')
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
SPRITES = {
|
||||||
|
'gpu_probidia': lambda: gpu('G', 'g2'),
|
||||||
|
'gpu_pmd': lambda: gpu('R', 'r2'),
|
||||||
|
'gpu_burnt': lambda: gpu('e', 'c', burnt=True),
|
||||||
|
'rig': rig,
|
||||||
|
'terminal': terminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if len(sys.argv) == 3 and sys.argv[1] == '--preview':
|
||||||
|
for name, make in SPRITES.items():
|
||||||
|
make().png(os.path.join(sys.argv[2], name + '.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
|
||||||
|
print(sys.argv[2])
|
||||||
|
else:
|
||||||
|
out = os.path.normpath(os.path.join(here, '..', 'src', 'Decentralized', 'ModFiles', 'Textures', 'Items'))
|
||||||
|
for name, make in SPRITES.items():
|
||||||
|
make().png(os.path.join(out, name + '.png'))
|
||||||
|
print(out)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
public static class ModInfo
|
||||||
|
{
|
||||||
|
public const string Id = "probably_decentralized";
|
||||||
|
public const string Name = "Probably Decentralized";
|
||||||
|
public const string Version = "0.1.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>本地化:游戏的 mod 本地化表(ModFiles/Localization/*.csv),当前语言缺失时回退英文。</summary>
|
||||||
|
public static class L
|
||||||
|
{
|
||||||
|
public static string T(string key, params object[] args) => ModHelper.GetLocalized(ModInfo.Id, key, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Log
|
||||||
|
{
|
||||||
|
public static void Info(string m) => Debug.Log($"[{ModInfo.Name}] {m}");
|
||||||
|
public static void Warn(string m) => Debug.LogWarning($"[{ModInfo.Name}] {m}");
|
||||||
|
public static void Error(string m) => Debug.LogError($"[{ModInfo.Name}] {m}");
|
||||||
|
|
||||||
|
public static void Guard(string what, Action a)
|
||||||
|
{
|
||||||
|
try { a(); }
|
||||||
|
catch (Exception ex) { Error($"{what}: {ex}"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Fmt
|
||||||
|
{
|
||||||
|
public static string Num(double v) => Math.Abs(v) >= 100 ? v.ToString("N0") : Math.Abs(v) >= 1 ? v.ToString("0.##") : v.ToString("0.####");
|
||||||
|
public static string Credits(double v) => v.ToString("N0");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!-- 游戏自带 mod 系统(Probably Stolen 完整版,Unity Mono)的 mod:IMod 入口,不打 Harmony 补丁,不需要任何能力。 -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<AssemblyName>ProbablyDecentralized</AssemblyName>
|
||||||
|
<RootNamespace>Decentralized</RootNamespace>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<!-- 游戏安装目录:默认是 Windows 上 Steam 的默认位置;其它位置用 dotnet build -p:GameDir=... 指定 -->
|
||||||
|
<GameDir Condition="'$(GameDir)' == ''">C:/Program Files (x86)/Steam/steamapps/common/Probably Stolen</GameDir>
|
||||||
|
<ManagedDir>$(GameDir)/Probably Stolen_Data/Managed</ManagedDir>
|
||||||
|
<ModInstallDir>$(GameDir)/Mods/ProbablyDecentralized</ModInstallDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- 游戏和 Unity 的程序集:只编译时引用,不复制 -->
|
||||||
|
<Reference Include="$(ManagedDir)/Assembly-CSharp.dll" Private="false" />
|
||||||
|
<Reference Include="$(ManagedDir)/UnityEngine*.dll" Private="false" />
|
||||||
|
<Reference Include="$(ManagedDir)/Unity.TextMeshPro.dll" Private="false" />
|
||||||
|
<Reference Include="$(ManagedDir)/Unity.Localization.dll" Private="false" />
|
||||||
|
<Reference Include="$(ManagedDir)/Newtonsoft.Json.dll" Private="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../Decentralized.Domain/Decentralized.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- 随 mod 发布的文件:清单、本地化表、物品图(art/sprites.py 生成) -->
|
||||||
|
<ModFile Include="ModFiles/**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- 编译后装进 <游戏>/Mods/ProbablyDecentralized/。只在游戏关闭时编译;只想编译、不安装加 -p:InstallMod=false。 -->
|
||||||
|
<Target Name="InstallMod" AfterTargets="Build" Condition="'$(InstallMod)' != 'false'">
|
||||||
|
<MakeDir Directories="$(ModInstallDir)" />
|
||||||
|
<Copy SourceFiles="$(TargetPath);$(TargetDir)ProbablyDecentralized.Domain.dll" DestinationFolder="$(ModInstallDir)" />
|
||||||
|
<Copy SourceFiles="@(ModFile)" DestinationFiles="@(ModFile->'$(ModInstallDir)/%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||||
|
<Message Importance="high" Text="Installed into $(ModInstallDir)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Decentralized.Domain;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// mod 物品:交易终端、矿架、显卡(每个型号一个物品 id)。显卡的健康度、假卡、翻新、烧毁存在物品标签上,随存档保存。
|
||||||
|
/// 读档时游戏按 id 重新调用工厂,再把存档里的标签与子物品(矿架里的显卡、电池)恢复回去。
|
||||||
|
/// </summary>
|
||||||
|
public static class Items
|
||||||
|
{
|
||||||
|
public const string Terminal = "pd_terminal";
|
||||||
|
public const string Rig = "pd_rig";
|
||||||
|
public const string GpuPrefix = "pd_gpu_";
|
||||||
|
|
||||||
|
public const string HealthTag = "PD_HEALTH";
|
||||||
|
public const string FakeTag = "PD_FAKE_OF";
|
||||||
|
public const string RefurbTag = "PD_REFURBISHED";
|
||||||
|
public const string DeadTag = "PD_DEAD";
|
||||||
|
|
||||||
|
public static Action OpenTerminal;
|
||||||
|
|
||||||
|
public static void Subscribe() => ModHook.OnModItemDirectoryInit += Register;
|
||||||
|
public static void Unsubscribe() => ModHook.OnModItemDirectoryInit -= Register;
|
||||||
|
|
||||||
|
public static string GpuId(string model) => GpuPrefix + model.ToLowerInvariant();
|
||||||
|
|
||||||
|
public static bool IsGpu(GameItem item) => item != null && item.identifier != null && item.identifier.StartsWith(GpuPrefix);
|
||||||
|
|
||||||
|
private static void Register(ModItemDirectory dir)
|
||||||
|
{
|
||||||
|
dir.Add(Terminal, CreateTerminal);
|
||||||
|
dir.Add(Rig, CreateRig);
|
||||||
|
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic))
|
||||||
|
{
|
||||||
|
var s = spec;
|
||||||
|
dir.Add(GpuId(s.Model), () => CreateGpu(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 交易终端 ----------
|
||||||
|
|
||||||
|
private static GameItem CreateTerminal()
|
||||||
|
{
|
||||||
|
var item = ItemDirectory.CreateEmptyItem(Terminal);
|
||||||
|
item.SetSpriteAndShapeFromMod(ModInfo.Id, "terminal");
|
||||||
|
item.name = L.T("item.terminal.name");
|
||||||
|
item.shortDescription = L.T("item.terminal.desc");
|
||||||
|
item.unitValue = 0;
|
||||||
|
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
|
||||||
|
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
|
||||||
|
item.mayActivateSlotItemFunc = (_, _, _) => true;
|
||||||
|
item.canActivateSlotItemFunc = (_, _, _) => true;
|
||||||
|
item.onActivateSlotItemFunc = (_, _, _) => Log.Guard("打开交易终端", () => OpenTerminal?.Invoke());
|
||||||
|
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 显卡 ----------
|
||||||
|
|
||||||
|
/// <summary>RZX70 → "RZX 70",RZX80Ti → "RZX 80 Ti",RZ6800 → "RZ 6800"。</summary>
|
||||||
|
public static string DisplayModel(string model) => Regex.Replace(Regex.Replace(model, @"(?<=[A-Za-z])(?=\d)", " "), @"(?<=\d)(?=[A-Z])", " ");
|
||||||
|
|
||||||
|
private static GameItem CreateGpu(HardwareSpec spec)
|
||||||
|
{
|
||||||
|
var item = ItemDirectory.CreateEmptyItem(GpuId(spec.Model));
|
||||||
|
item.SetSpriteAndShapeFromMod(ModInfo.Id, spec.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd");
|
||||||
|
item.SetGameItemType("ELECTRONIC");
|
||||||
|
item.onLoaded = Refresh;
|
||||||
|
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
|
||||||
|
Refresh(item);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Unit ToUnit(GameItem item)
|
||||||
|
{
|
||||||
|
string model = item.identifier.Substring(GpuPrefix.Length);
|
||||||
|
model = Hardware.Specs.Keys.First(k => k.ToLowerInvariant() == model);
|
||||||
|
var u = new Unit { Model = model, Health = 100 };
|
||||||
|
if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
|
||||||
|
if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString();
|
||||||
|
u.Refurbished = item.IsTag(RefurbTag);
|
||||||
|
u.Dead = item.IsTag(DeadTag);
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Store(GameItem item, Unit u)
|
||||||
|
{
|
||||||
|
item.ModifyTag(HealthTag, s => s.Enable().SetDouble(u.Health));
|
||||||
|
if (u.FakeOf != null) item.ModifyTag(FakeTag, s => s.Enable().SetString(u.FakeOf));
|
||||||
|
if (u.Refurbished) item.EnableTag(RefurbTag);
|
||||||
|
if (u.Dead) item.EnableTag(DeadTag);
|
||||||
|
Refresh(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按标签更新名字、说明、价值和图:显示的是标签上的型号(假卡也显示被冒充的型号)。</summary>
|
||||||
|
public static void Refresh(GameItem item)
|
||||||
|
{
|
||||||
|
var u = ToUnit(item);
|
||||||
|
var label = u.Label;
|
||||||
|
if (u.Dead)
|
||||||
|
{
|
||||||
|
item.name = L.T("item.gpu.burnt.name", label.Brand, DisplayModel(label.Model));
|
||||||
|
item.shortDescription = L.T("item.gpu.burnt.desc");
|
||||||
|
item.unitValue = Math.Max(1, label.Price / 20);
|
||||||
|
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_burnt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.name = L.T("item.gpu.name", label.Brand, DisplayModel(label.Model));
|
||||||
|
item.shortDescription = L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), Mathf.RoundToInt((float)u.Health));
|
||||||
|
item.unitValue = Math.Max(1, (long)Math.Round(label.Price * u.Health / 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 矿架 ----------
|
||||||
|
|
||||||
|
private static GameItem CreateRig()
|
||||||
|
{
|
||||||
|
var battery = new GameSlotInventory();
|
||||||
|
var cards = new GameGridInventory(2, 4);
|
||||||
|
var window = MachineHelper.CreateMachineryWindow(true,
|
||||||
|
MachineHelper.PowerPanel(battery),
|
||||||
|
MachineHelper.Panel(L.T("rig.label.cards"), cards, "cards"));
|
||||||
|
var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window);
|
||||||
|
rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig");
|
||||||
|
rig.name = L.T("item.rig.name");
|
||||||
|
rig.unitValue = Tuning.RigPrice;
|
||||||
|
MachineHelper.SetupBatterySlot(battery, rig);
|
||||||
|
rig.EnableTag("CONTAINER_TAG");
|
||||||
|
rig.EnableTag("STANDARD_MACHINE_TAG");
|
||||||
|
MachineryHelper.InitMachinery(rig, Tuning.RigKj);
|
||||||
|
cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag);
|
||||||
|
cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, cards);
|
||||||
|
cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, cards);
|
||||||
|
rig.onLoaded = _ => UpdateRig(rig, cards);
|
||||||
|
rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, cards));
|
||||||
|
ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name);
|
||||||
|
UpdateRig(rig, cards);
|
||||||
|
return rig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<GameItem> Cards(GameInventory cards) => cards.childItems.Where(IsGpu).ToList();
|
||||||
|
|
||||||
|
private static int RigKj(GameInventory cards) =>
|
||||||
|
Tuning.RigKj + Mathf.CeilToInt((float)Cards(cards).Select(ToUnit).Sum(u => u.Kj(false)));
|
||||||
|
|
||||||
|
private static void UpdateRig(GameItem rig, GameInventory cards)
|
||||||
|
{
|
||||||
|
int kj = RigKj(cards);
|
||||||
|
rig.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(kj));
|
||||||
|
double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(false));
|
||||||
|
rig.shortDescription = L.T("item.rig.desc", Fmt.Num(hash * 10), kj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>夜间早段:按矿架上的显卡总耗电从电池扣电;扣电成功,登记今晚参与挖矿的显卡。</summary>
|
||||||
|
private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory cards)
|
||||||
|
{
|
||||||
|
var list = Cards(cards).Where(c => !c.IsTag(DeadTag)).ToList();
|
||||||
|
Session.TonightRigs++;
|
||||||
|
if (list.Count == 0) return;
|
||||||
|
int kj = RigKj(cards);
|
||||||
|
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return;
|
||||||
|
Session.TonightPoweredRigs++;
|
||||||
|
Session.TonightKj += kj;
|
||||||
|
Session.TonightCards.AddRange(list);
|
||||||
|
UpdateRig(rig, cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 放进储藏区(游戏 EmporiumTryAdd 的默认目标,放不下时游戏自己找位置)。
|
||||||
|
/// 不放柜台:测试中柜台上的物品过夜后会消失(未在正常流程中确认);ModHelper.MoveItemTo* 放满时会静默失败。
|
||||||
|
/// </summary>
|
||||||
|
public static void Give(GameItem item)
|
||||||
|
{
|
||||||
|
GeneralHelper.SetItemOwned(item, true);
|
||||||
|
GraphUtils.EmporiumTryAdd(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// mod 入口(游戏自带 mod 系统)。Init / OnEnable 在主菜单阶段调用:这里只订阅钩子、登记物品和人脉升级,
|
||||||
|
/// 不碰存档和店里的东西(那些在读档 / 新游戏的钩子里做)。
|
||||||
|
/// </summary>
|
||||||
|
public class ModEntry : IMod
|
||||||
|
{
|
||||||
|
public void Init(ModManifest manifest) { }
|
||||||
|
|
||||||
|
public void OnEnable()
|
||||||
|
{
|
||||||
|
Items.Subscribe();
|
||||||
|
Items.OpenTerminal = TerminalWindow.Open;
|
||||||
|
Unlock.Register();
|
||||||
|
Night.Subscribe();
|
||||||
|
ModHook.OnGameLoadedLate += Session.Load;
|
||||||
|
ModHook.OnNewGameStarted += OnNewGame;
|
||||||
|
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDisable()
|
||||||
|
{
|
||||||
|
Items.Unsubscribe();
|
||||||
|
Night.Unsubscribe();
|
||||||
|
ModHook.OnGameLoadedLate -= Session.Load;
|
||||||
|
ModHook.OnNewGameStarted -= OnNewGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnNewGame(string scenarioId) => Session.Load();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Probably Decentralized localization table: English (en)
|
||||||
|
# Source language and fallback. Format: key,text (text may be quoted; \n is a line break).
|
||||||
|
upgrade.title,Mining Access
|
||||||
|
upgrade.card,Mining
|
||||||
|
upgrade.desc,Delivers a trading terminal and a mining rig to the back counter. Graphics cards installed in a rig mine PSCoin each night the rig has power.
|
||||||
|
item.terminal.name,Trading Terminal
|
||||||
|
item.terminal.desc,Wallet, exchange and the nightly mining report.
|
||||||
|
item.rig.name,Mining Rig
|
||||||
|
item.rig.desc,"Holds up to four graphics cards. Hashrate {0} MH/s. Draws {1} kJ per day."
|
||||||
|
rig.label.cards,Graphics cards
|
||||||
|
item.gpu.name,{0} {1}
|
||||||
|
item.gpu.desc,"Hashrate {0} MH/s. Draws {1} kJ per day. Condition {2}%."
|
||||||
|
item.gpu.burnt.name,Burnt {0} {1}
|
||||||
|
item.gpu.burnt.desc,This card no longer works.
|
||||||
|
term.title,Trading Terminal
|
||||||
|
tab.wallet,Wallet
|
||||||
|
tab.trade,Trade
|
||||||
|
tab.report,Report
|
||||||
|
wallet.line,"{0}: {1} (about {2} credits)"
|
||||||
|
wallet.empty,The wallet is empty.
|
||||||
|
wallet.total,Total value: {0} credits
|
||||||
|
wallet.taint,Tainted value: {0} credits
|
||||||
|
trade.amount,Amount
|
||||||
|
trade.hint,Buy spends the amount in the quote coin. Sell sells the amount of the traded coin.
|
||||||
|
trade.deposit,Deposit credits as CRC
|
||||||
|
trade.withdraw,Withdraw CRC as credits
|
||||||
|
trade.pair,"{0}/{1} ({2})"
|
||||||
|
trade.price,Price {0}
|
||||||
|
trade.buy,Buy
|
||||||
|
trade.sell,Sell
|
||||||
|
venue.Exchange,Exchange
|
||||||
|
venue.Dex,DEX
|
||||||
|
venue.Darknet,Darknet
|
||||||
|
msg.amount,Enter an amount.
|
||||||
|
msg.balance,Not enough balance.
|
||||||
|
msg.trade,"Paid {0} {1}, received {2} {3}."
|
||||||
|
msg.deposit,Deposited {0} credits as CRC.
|
||||||
|
msg.cashout,"Received {0} credits. Tax {1} credits."
|
||||||
|
msg.aml_light,Compliance review: {0} credits of tainted funds were withheld.
|
||||||
|
msg.aml_heavy,Compliance review: {0} credits of tainted funds were seized and reported to Security.
|
||||||
|
report.none,No mining report yet.
|
||||||
|
report.day,Night of day {0}
|
||||||
|
report.rigs,Rigs with power: {0} of {1}
|
||||||
|
report.hash,"Hashrate: {0} MH/s (network {1} MH/s)"
|
||||||
|
report.mined,"Mined: {0} PSC (about {1} credits)"
|
||||||
|
report.energy,Energy used: {0} kJ
|
||||||
|
report.burnt,Cards burnt out: {0}
|
||||||
|
report.price,PSCoin price: {0} credits
|
||||||
|
nightlog.mined,Mining rigs produced {0} PSC.
|
||||||
|
nightlog.burnt,A {0} burnt out.
|
||||||
|
nightlog.unpowered,{0} mining rig(s) had no power.
|
||||||
|
news.open.title,PSCoin Exchange Opens
|
||||||
|
news.open.desc,The Upper Level exchange now lists PSCoin against CRC. Graphics cards are in demand across the station.
|
||||||
|
news.halving.title,PSCoin Block Reward Halved
|
||||||
|
news.halving.desc,Miners now receive half as many coins per night.
|
||||||
|
@@ -0,0 +1,54 @@
|
|||||||
|
# Probably Decentralized 本地化表:简体中文(zh)
|
||||||
|
upgrade.title,矿业渠道
|
||||||
|
upgrade.card,挖矿
|
||||||
|
upgrade.desc,交付一台交易终端和一个矿架,放在后柜台。装进矿架的显卡在有电的夜晚挖出 PSCoin。
|
||||||
|
item.terminal.name,交易终端
|
||||||
|
item.terminal.desc,钱包、交易所与夜间挖矿报告。
|
||||||
|
item.rig.name,矿架
|
||||||
|
item.rig.desc,可装 4 张显卡。算力 {0} MH/s。耗电 {1} kJ/天。
|
||||||
|
rig.label.cards,显卡
|
||||||
|
item.gpu.name,{0} {1}
|
||||||
|
item.gpu.desc,算力 {0} MH/s。耗电 {1} kJ/天。健康度 {2}%。
|
||||||
|
item.gpu.burnt.name,烧毁的 {0} {1}
|
||||||
|
item.gpu.burnt.desc,此显卡已无法工作。
|
||||||
|
term.title,交易终端
|
||||||
|
tab.wallet,钱包
|
||||||
|
tab.trade,交易
|
||||||
|
tab.report,报告
|
||||||
|
wallet.line,{0}:{1}(约 {2} 信用点)
|
||||||
|
wallet.empty,钱包为空。
|
||||||
|
wallet.total,总值:{0} 信用点
|
||||||
|
wallet.taint,污点:{0} 信用点
|
||||||
|
trade.amount,数量
|
||||||
|
trade.hint,买入按计价币的数量支付;卖出按所交易币的数量卖出。
|
||||||
|
trade.deposit,存入信用点(换成 CRC)
|
||||||
|
trade.withdraw,提取 CRC(换成信用点)
|
||||||
|
trade.pair,{0}/{1}({2})
|
||||||
|
trade.price,价格 {0}
|
||||||
|
trade.buy,买入
|
||||||
|
trade.sell,卖出
|
||||||
|
venue.Exchange,交易所
|
||||||
|
venue.Dex,DEX
|
||||||
|
venue.Darknet,暗网
|
||||||
|
msg.amount,请输入数量。
|
||||||
|
msg.balance,余额不足。
|
||||||
|
msg.trade,支付 {0} {1},得到 {2} {3}。
|
||||||
|
msg.deposit,已存入 {0} 信用点,换成 CRC。
|
||||||
|
msg.cashout,到账 {0} 信用点。税 {1} 信用点。
|
||||||
|
msg.aml_light,合规审查:{0} 信用点的污点资金被扣留。
|
||||||
|
msg.aml_heavy,合规审查:{0} 信用点的污点资金被没收,并已上报安保部。
|
||||||
|
report.none,暂无挖矿报告。
|
||||||
|
report.day,第 {0} 天夜间
|
||||||
|
report.rigs,有电的矿架:{0} / {1}
|
||||||
|
report.hash,算力:{0} MH/s(全网 {1} MH/s)
|
||||||
|
report.mined,挖出:{0} PSC(约 {1} 信用点)
|
||||||
|
report.energy,耗电:{0} kJ
|
||||||
|
report.burnt,烧毁的显卡:{0}
|
||||||
|
report.price,PSCoin 价格:{0} 信用点
|
||||||
|
nightlog.mined,矿架挖出 {0} PSC。
|
||||||
|
nightlog.burnt,一张 {0} 烧毁了。
|
||||||
|
nightlog.unpowered,{0} 个矿架没有电。
|
||||||
|
news.open.title,PSCoin 交易所开业
|
||||||
|
news.open.desc,上层交易所上架 PSCoin/CRC 交易对。全站显卡需求上升。
|
||||||
|
news.halving.title,PSCoin 出块奖励减半
|
||||||
|
news.halving.desc,矿工每晚获得的币减半。
|
||||||
|
Binary file not shown.
|
After Width: | Height: | Size: 229 B |
Binary file not shown.
|
After Width: | Height: | Size: 185 B |
Binary file not shown.
|
After Width: | Height: | Size: 185 B |
Binary file not shown.
|
After Width: | Height: | Size: 255 B |
Binary file not shown.
|
After Width: | Height: | Size: 234 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Manifest>
|
||||||
|
<ID>probably_decentralized</ID>
|
||||||
|
<Name>Probably Decentralized</Name>
|
||||||
|
<Author>edward</Author>
|
||||||
|
<ModVersion>0.1.0</ModVersion>
|
||||||
|
<Description>Mine PSCoin with graphics cards, trade it on a fictional exchange, and cash it out past the compliance desk. Work in progress. All coins, exchanges and events are fictional and have no real-world value.</Description>
|
||||||
|
<GameVersions>
|
||||||
|
<Version>0.46</Version>
|
||||||
|
</GameVersions>
|
||||||
|
<Prerequisites />
|
||||||
|
<Assemblies>
|
||||||
|
<Assembly>ProbablyDecentralized.dll</Assembly>
|
||||||
|
<Assembly>ProbablyDecentralized.Domain.dll</Assembly>
|
||||||
|
</Assemblies>
|
||||||
|
<Capabilities />
|
||||||
|
</Manifest>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Settle()
|
||||||
|
{
|
||||||
|
var d = Session.Data;
|
||||||
|
int day = StoreStation.GetDayCounter();
|
||||||
|
var events = ActiveEvents();
|
||||||
|
foreach (var ev in events.Where(e => !d.ActiveEvents.Contains(e))) d.Stable.OnEventStart(ev, day, d.Rng);
|
||||||
|
d.ActiveEvents = events;
|
||||||
|
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, unit: Items.ToUnit(c))).ToList();
|
||||||
|
double hash = units.Sum(x => x.unit.Hash(false));
|
||||||
|
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) in units)
|
||||||
|
{
|
||||||
|
bool died = unit.Night(false, 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) 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) d.Hot.Balances[Assets.CRC] = d.Hot.Get(Assets.CRC) * crcFactor;
|
||||||
|
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 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");
|
||||||
|
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>(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Decentralized.Domain;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
/// <summary>上一晚的挖矿报告(终端"报告"页和晨间日志)。</summary>
|
||||||
|
public sealed class NightReport
|
||||||
|
{
|
||||||
|
public int Day;
|
||||||
|
public int Rigs, PoweredRigs;
|
||||||
|
public double Hash, NetworkHash, Mined, Price, Kj;
|
||||||
|
public List<string> Burnt = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>一局的 mod 状态。整体序列化成 JSON 存进游戏的 mod 存档数据;显卡的健康度等存在显卡物品的标签上。</summary>
|
||||||
|
public sealed class SaveData
|
||||||
|
{
|
||||||
|
public int Version = 1;
|
||||||
|
public int Day;
|
||||||
|
public Rng Rng = new Rng((ulong)DateTime.UtcNow.Ticks);
|
||||||
|
public Market Market;
|
||||||
|
public Stablecoins Stable = new Stablecoins();
|
||||||
|
public Network Network = new Network();
|
||||||
|
public Wallet Hot = new Wallet { Address = "hot" };
|
||||||
|
public Exchange Exchange = new Exchange();
|
||||||
|
public List<MarketEvent> ActiveEvents = new List<MarketEvent>();
|
||||||
|
public NightReport LastNight;
|
||||||
|
public bool OpeningNewsShown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>当前这一局的状态:读档 / 新游戏时载入,睡觉结算后写回。</summary>
|
||||||
|
public static class Session
|
||||||
|
{
|
||||||
|
private const string Key = "state";
|
||||||
|
public static SaveData Data;
|
||||||
|
|
||||||
|
/// <summary>今晚有电的矿架上的显卡(矿架在夜间早段扣电时登记,结算时用)。</summary>
|
||||||
|
public static readonly List<GameItem> TonightCards = new List<GameItem>();
|
||||||
|
public static int TonightRigs, TonightPoweredRigs;
|
||||||
|
public static double TonightKj;
|
||||||
|
|
||||||
|
public static void Load()
|
||||||
|
{
|
||||||
|
Data = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = ModHelper.GetModData(ModInfo.Id, Key);
|
||||||
|
if (!string.IsNullOrEmpty(json)) Data = JsonConvert.DeserializeObject<SaveData>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Log.Error($"读取存档数据失败,重新开始:{ex.Message}"); }
|
||||||
|
if (Data == null) Data = new SaveData();
|
||||||
|
if (Data.Market == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter());
|
||||||
|
Log.Info($"状态已载入(第 {Data.Day} 天,PSCoin {Data.Market.Price:0.00})");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save()
|
||||||
|
{
|
||||||
|
if (Data == null) return;
|
||||||
|
try { ModHelper.SetModData(ModInfo.Id, Key, JsonConvert.SerializeObject(Data)); }
|
||||||
|
catch (Exception ex) { Log.Error($"写入存档数据失败:{ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>链上资产的信用点价格。</summary>
|
||||||
|
public static double Price(string asset)
|
||||||
|
{
|
||||||
|
if (asset == Assets.PSC) return Data.Market.Price;
|
||||||
|
var spec = Assets.Get(asset);
|
||||||
|
if (spec != null && spec.Stable) return Data.Stable.Price(asset);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double Rent() => PlayerStore.Instance != null ? PlayerStore.Instance.rentValue : 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Decentralized.Domain;
|
||||||
|
|
||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
/// <summary>交易终端窗口:钱包、交易、报告三页。每次操作后按当前页重建窗口。</summary>
|
||||||
|
public static class TerminalWindow
|
||||||
|
{
|
||||||
|
public const string Id = "pd_terminal";
|
||||||
|
private enum Page { Wallet, Trade, Report }
|
||||||
|
|
||||||
|
private static Page _page = Page.Wallet;
|
||||||
|
private static string _amount = "";
|
||||||
|
private static string _message;
|
||||||
|
|
||||||
|
public static void Open()
|
||||||
|
{
|
||||||
|
if (Session.Data == null) return;
|
||||||
|
var b = CustomUIManager.Window(Id, L.T("term.title"));
|
||||||
|
if (b == null) return;
|
||||||
|
b.SetSize(620, 560).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
|
||||||
|
b.BeginRow();
|
||||||
|
foreach (Page p in Enum.GetValues(typeof(Page)))
|
||||||
|
{
|
||||||
|
var page = p;
|
||||||
|
string name = L.T("tab." + p.ToString().ToLowerInvariant());
|
||||||
|
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; Open(); });
|
||||||
|
}
|
||||||
|
b.End();
|
||||||
|
b.AddSeparator();
|
||||||
|
b.BeginScroll(440);
|
||||||
|
switch (_page)
|
||||||
|
{
|
||||||
|
case Page.Wallet: WalletPage(b); break;
|
||||||
|
case Page.Trade: TradePage(b); break;
|
||||||
|
default: ReportPage(b); break;
|
||||||
|
}
|
||||||
|
b.End();
|
||||||
|
if (!string.IsNullOrEmpty(_message)) b.AddLabel(_message);
|
||||||
|
b.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WalletPage(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
var w = Session.Data.Hot;
|
||||||
|
if (w.Balances.Count == 0) b.AddLabel(L.T("wallet.empty"));
|
||||||
|
foreach (var kv in w.Balances.OrderBy(k => k.Key))
|
||||||
|
b.AddLabel(L.T("wallet.line", kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))));
|
||||||
|
b.AddSeparator();
|
||||||
|
b.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
|
||||||
|
if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TradePage(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
b.AddLabel(L.T("trade.hint"));
|
||||||
|
b.BeginRow();
|
||||||
|
b.AddLabel(L.T("trade.amount"));
|
||||||
|
b.AddInput("0", _amount, s => _amount = s, "amount", s => _amount = s);
|
||||||
|
b.End();
|
||||||
|
b.BeginRow();
|
||||||
|
b.AddButton(L.T("trade.deposit"), Deposit);
|
||||||
|
b.AddButton(L.T("trade.withdraw"), Withdraw);
|
||||||
|
b.End();
|
||||||
|
b.AddSeparator();
|
||||||
|
foreach (var pool in Session.Data.Market.Pools)
|
||||||
|
{
|
||||||
|
var p = pool;
|
||||||
|
b.AddLabel(L.T("trade.pair", p.Base, p.Quote, L.T("venue." + p.Venue)) + " " + L.T("trade.price", Fmt.Num(p.Price)));
|
||||||
|
b.BeginRow();
|
||||||
|
b.AddButton(L.T("trade.buy"), () => Trade(p, buy: true));
|
||||||
|
b.AddButton(L.T("trade.sell"), () => Trade(p, buy: false));
|
||||||
|
b.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportPage(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
var r = Session.Data.LastNight;
|
||||||
|
if (r == null) { b.AddLabel(L.T("report.none")); return; }
|
||||||
|
b.AddHeader(L.T("report.day", r.Day));
|
||||||
|
b.AddLabel(L.T("report.rigs", r.PoweredRigs, r.Rigs));
|
||||||
|
b.AddLabel(L.T("report.hash", Fmt.Num(r.Hash * 10), Fmt.Num(r.NetworkHash * 10)));
|
||||||
|
b.AddLabel(L.T("report.mined", Fmt.Num(r.Mined), Fmt.Credits(r.Mined * r.Price)));
|
||||||
|
b.AddLabel(L.T("report.energy", Fmt.Num(r.Kj)));
|
||||||
|
b.AddLabel(L.T("report.burnt", r.Burnt.Count));
|
||||||
|
foreach (var n in r.Burnt) b.AddLabel(" " + n);
|
||||||
|
b.AddLabel(L.T("report.price", Fmt.Num(r.Price)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReadAmount(out double v)
|
||||||
|
{
|
||||||
|
if (!double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) || v <= 0)
|
||||||
|
{
|
||||||
|
_message = L.T("msg.amount");
|
||||||
|
Open();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Deposit()
|
||||||
|
{
|
||||||
|
if (!ReadAmount(out double v)) return;
|
||||||
|
int credits = (int)Math.Floor(v);
|
||||||
|
if (credits <= 0 || PlayerStore.Instance.playerCash < credits) { _message = L.T("msg.balance"); Open(); return; }
|
||||||
|
ModHelper.AddCredit(-credits, false);
|
||||||
|
var w = Session.Data.Hot;
|
||||||
|
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
|
||||||
|
w.Basis += credits;
|
||||||
|
_message = L.T("msg.deposit", credits);
|
||||||
|
Session.Save();
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Withdraw()
|
||||||
|
{
|
||||||
|
if (!ReadAmount(out double v)) return;
|
||||||
|
var d = Session.Data;
|
||||||
|
if (d.Hot.Get(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||||
|
var r = d.Exchange.Cashout(d.Hot, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), false, d.Rng);
|
||||||
|
int got = (int)Math.Floor(r.Received - r.Tax);
|
||||||
|
ModHelper.AddCredit(got, true);
|
||||||
|
_message = L.T("msg.cashout", got, Fmt.Credits(r.Tax));
|
||||||
|
if (r.Outcome == AmlOutcome.Light) _message += "\n" + L.T("msg.aml_light", Fmt.Credits(r.Confiscated));
|
||||||
|
if (r.Outcome == AmlOutcome.Heavy)
|
||||||
|
{
|
||||||
|
_message += "\n" + L.T("msg.aml_heavy", Fmt.Credits(r.Confiscated));
|
||||||
|
PlayerStore.Instance.secData.CommitCrime("FENCING", (int)Math.Round(r.Crime));
|
||||||
|
}
|
||||||
|
Session.Save();
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Trade(Pool p, bool buy)
|
||||||
|
{
|
||||||
|
if (!ReadAmount(out double v)) return;
|
||||||
|
var w = Session.Data.Hot;
|
||||||
|
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
|
||||||
|
if (w.Get(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||||
|
double got = buy ? p.BuyBase(v) : p.SellBase(v);
|
||||||
|
w.Add(pay, -v);
|
||||||
|
w.Add(get, got);
|
||||||
|
_message = L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
|
||||||
|
Session.Save();
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
/// <summary>人脉网络里的"矿业渠道":买下后交付交易终端和一个矿架,并开始每天的行情与挖矿结算。</summary>
|
||||||
|
public static class Unlock
|
||||||
|
{
|
||||||
|
public const string Id = "PD_MINING_ACCESS";
|
||||||
|
public const int Price = 300;
|
||||||
|
|
||||||
|
public static bool IsUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(Id);
|
||||||
|
|
||||||
|
public static void Register()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 文字都用回调:OnEnable 时 mod 本地化表还没加载
|
||||||
|
ModHelper.AddNetworkUpgrade(ModInfo.Id, Id, Price, "",
|
||||||
|
() => L.T("upgrade.title"), () => L.T("upgrade.desc"), OnPurchased,
|
||||||
|
iconKey: ModInfo.Id + ":rig", subtitleLabel: () => L.T("upgrade.card"));
|
||||||
|
}
|
||||||
|
catch (NullReferenceException)
|
||||||
|
{
|
||||||
|
// 登记定义后游戏会访问 PlayerStore.Instance,主菜单阶段为空会抛异常;定义已登记,读档时游戏会把升级补进这一局。
|
||||||
|
Log.Info("人脉升级已登记(主菜单阶段访问 PlayerStore 失败,已忽略)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnPurchased()
|
||||||
|
{
|
||||||
|
Items.Give(ModHelper.SpawnItem(Items.Terminal));
|
||||||
|
Items.Give(ModHelper.SpawnItem(Items.Rig));
|
||||||
|
Items.Give(ModHelper.SpawnItem(Items.GpuId("RZX50")));
|
||||||
|
Log.Info("已解锁矿业渠道");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -276,7 +276,43 @@ internal static class Program
|
|||||||
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
|
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Test("存档:市场、稳定币、全网、钱包、交易所、随机数序列化后读回,之后的结果一致", () =>
|
||||||
|
{
|
||||||
|
var m = Market.CreateDefault(3);
|
||||||
|
var st = new Stablecoins();
|
||||||
|
var net = new Network();
|
||||||
|
var w = new Wallet { Address = "hot", Taint = 12, Basis = 34 };
|
||||||
|
w.Add(Assets.PSC, 5);
|
||||||
|
var ex = new Exchange();
|
||||||
|
ex.History.Add(new KeyValuePair<int, double>(2, 100));
|
||||||
|
var rng = new Rng(77);
|
||||||
|
for (int d = 1; d <= 5; d++) { m.Night(new MarketEvent[0], rng, d, a => 1); st.Night(d, rng, 0); net.Night(d, m.Price, 10); }
|
||||||
|
var bundle = new SaveBundle { Market = m, Stable = st, Network = net, Hot = w, Exchange = ex, Rng = rng };
|
||||||
|
string json = Newtonsoft.Json.JsonConvert.SerializeObject(bundle);
|
||||||
|
var back = Newtonsoft.Json.JsonConvert.DeserializeObject<SaveBundle>(json);
|
||||||
|
Check(Near(back.Market.Price, m.Price, 1e-9) && back.Market.Pools.Count == m.Pools.Count && back.Market.Pools[1].Id == m.Pools[1].Id, "交易对与价格");
|
||||||
|
Check(back.Hot.Get(Assets.PSC) == 5 && back.Hot.Taint == 12 && back.Hot.Basis == 34, "钱包余额、污点、成本");
|
||||||
|
Check(back.Exchange.History.Count == 1 && back.Exchange.History[0].Value == 100, "出金记录");
|
||||||
|
Check(Near(back.Network.NpcHash, net.NpcHash, 1e-9) && Near(back.Stable.CraSubsidy, st.CraSubsidy, 1e-9), "全网算力、CRA 补贴池");
|
||||||
|
for (int d = 6; d <= 10; d++)
|
||||||
|
{
|
||||||
|
m.Night(new MarketEvent[0], rng, d, a => 1);
|
||||||
|
back.Market.Night(new MarketEvent[0], back.Rng, d, a => 1);
|
||||||
|
}
|
||||||
|
Check(Near(back.Market.Price, m.Price, 1e-9), "读回后继续推进,行情与原来完全相同");
|
||||||
|
});
|
||||||
|
|
||||||
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
||||||
return _failed == 0 ? 0 : 1;
|
return _failed == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class SaveBundle
|
||||||
|
{
|
||||||
|
public Market Market;
|
||||||
|
public Stablecoins Stable;
|
||||||
|
public Network Network;
|
||||||
|
public Wallet Hot;
|
||||||
|
public Exchange Exchange;
|
||||||
|
public Rng Rng;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user