Lethal Company Mod Zehs-StreamOverlaysを例としたMod改変手順

Zehs-StreamOverlaysの手元での改変を試したので、他のMod改変で共通して使える知見として、メモしておきます。 今回は公開を目的としたものではなく、知見も持っていないので、Thunderstoreへの公開は内容に含まれていません。 Zehs-StreamOverlaysのソースコードは、MIT Licenseで配布されており、改変箇所を示すため、一部引用します。 GitHub Thunderstore バージョン情報 以下のバージョンを想定しています。 Lethal Company v72 (Build ID: 18916695) Windows 11 24H2 .NET SDK 9.0.201 BepInEx v5.4.21 ZehsTeam/Lethal-Company-StreamOverlays@8ba3632 BeplnEx プラグイン開発環境の設定 BeplnEx公式チュートリアルの開発環境の設定手順に従って、プラグイン開発環境を構築します。 Basic plugin: Setting up the development environment | BepInEx Docs GitHub: 記事執筆時点のソース@ca4a997 依存関係のセットアップ Thunderstore: LethalConfig v1.4.6 Thunderstore: ShipInventoryUpdated v1.2.13 Thunderstore: CSync v5.0.1 Thunderstoreの「Manual Download」からzipファイルをダウンロードして、中のDLLファイルを取り出します。 そのままビルドに使うと、以下のようなエラーが発生します。 $ DOTNET_CLI_UI_LANGUAGE=en dotnet build --configuration Release Restore complete (0.5s) StreamOverlays failed with 4 error(s) (0.3s) D:\workspaces\lethal_company_modding_workspace\Lethal-Company-StreamOverlays\StreamOverlays\Dependencies\ShipInventoryProxy\Patches\ChuteInteractPatch.cs(10,40): error CS0117: 'ChuteInteract' does not contain a definition for 'SpawnItemClientRpc' D:\workspaces\lethal_company_modding_workspace\Lethal-Company-StreamOverlays\StreamOverlays\Dependencies\ShipInventoryProxy\Patches\ItemManagerPatch.cs(10,38): error CS0117: 'ItemManager' does not contain a definition for 'UpdateCache' D:\workspaces\lethal_company_modding_workspace\Lethal-Company-StreamOverlays\StreamOverlays\Dependencies\ShipInventoryProxy\ShipInventoryProxy.cs(12,39): error CS0122: 'LCMPluginInfo' is inaccessible due to its protection level D:\workspaces\lethal_company_modding_workspace\Lethal-Company-StreamOverlays\StreamOverlays\Plugin.cs(16,18): error CS0182: An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type Build failed with 4 error(s) in 1.1s BepInEx.AssemblyPublicizerを使って、エラーの原因のアクセス制御を無効化したDLLを作成します。これらのDLLはビルド時のみ使用します。 ...

2025年8月21日 · aoirint

Lethal Company コンペティション Museum%に関するメモ

リーダーボード https://www.speedrun.com/Lethal_Company https://www.speedrun.com/Lethal_Company_Category_Extensions https://www.speedrun.com/Lethal_Company_Modded Museum% 達成条件 スクラップ全種かつ全バリエーションの船への保持 バリエーションのあるスクラップの一覧(v72) Easter egg (5種) Flask (2種) Painting (2種) Coffee mug (5種) Tattered metal sheet (2種) 追跡用MOD https://thunderstore.io/c/lethal-company/p/WarperSan/LethalMuseum/ 参考 https://www.speedrun.com/Lethal_Company/news/r655gv6v https://www.youtube.com/watch?v=pLwq3RJeRoE

2025年8月18日 · aoirint

Lethal Company Quotaの計算 v72 (Build ID: 18916695)

Lethal Company v72 (Build ID: 18916695)時点のQuotaの計算に関する仕様の解析メモです。 Quotaの増加量のランダム性 Quotaの増加量のランダム性は、TimeOfDay.quotaVariables.randomizerCurveで定義される、右肩上がりのランダマイザ曲線によってもたらされます。 このランダマイザ曲線は、以下の制御点を持つエルミート曲線です。 x y in_tangent out_tangent 0 -0.5030289 7.455404 7.455404 0.117235 -0.1301773 0.5548811 0.5548811 0.8803625 0.1534421 0.5221589 0.5221589 1 0.5030365 7.051469 7.051469 Unity 2022.3.9f1のAnimationCurveが使われています。 preWrapModeおよびpostWrapModeは、ClampForeverに設定されています。 これにより、[0, 1]の外側の評価値は、端点の値になります。 https://docs.unity3d.com/2022.3/Documentation/ScriptReference/WrapMode.ClampForever.html ランダマイザ曲線の描画スクリプト(Python) Details from dataclasses import dataclass from typing import List import bisect import numpy as np import matplotlib.pyplot as plt from matplotlib.ticker import MultipleLocator # ---- Unity AnimationCurve Keyframes ---- # pre_wrap, post_wrap: ClampForever KEYFRAMES_RAW = [ {"time": 0.0, "value": -0.5030289, "inTangent": 7.455404, "outTangent": 7.455404}, {"time": 0.117235, "value": -0.1301773, "inTangent": 0.5548811, "outTangent": 0.5548811}, {"time": 0.8803625, "value": 0.1534421, "inTangent": 0.5221589, "outTangent": 0.5221589}, {"time": 1.0, "value": 0.5030365, "inTangent": 7.051469, "outTangent": 7.051469}, ] @dataclass class Key: time: float value: float inTangent: float outTangent: float # ---- Unity-style Hermite evaluator (non-weighted) ---- class Curve: def __init__(self, keys: List[Key]): self.keys = sorted(keys, key=lambda k: k.time) def evaluate(self, t: float) -> float: if not self.keys: return 0.0 if len(self.keys) == 1: return self.keys[0].value times = [k.time for k in self.keys] # ClampForever: 範囲外は端の値を返す if t <= self.keys[0].time: return self.keys[0].value if t >= self.keys[-1].time: return self.keys[-1].value i = bisect.bisect_right(times, t) - 1 k0 = self.keys[i] k1 = self.keys[i + 1] t0, t1 = k0.time, k1.time y0, y1 = k0.value, k1.value dx = t1 - t0 if dx == 0.0: return y1 u = (t - t0) / dx m0 = k0.outTangent m1 = k1.inTangent h00 = 2*u**3 - 3*u**2 + 1 h10 = u**3 - 2*u**2 + u h01 = -2*u**3 + 3*u**2 h11 = u**3 - u**2 return h00*y0 + h10*(dx*m0) + h01*y1 + h11*(dx*m1) def main(): # Build curve curve = Curve([Key(**k) for k in KEYFRAMES_RAW]) # Sample and plot # 右に5%の外挿を含めてサンプリング t_min = KEYFRAMES_RAW[0]["time"] t_max = KEYFRAMES_RAW[-1]["time"] t_range = t_max - t_min ts = np.linspace(t_min, t_max + t_range * 0.05, 500) ys = np.array([curve.evaluate(t) for t in ts]) plt.figure( figsize=(16, 9), dpi=300, facecolor='white', edgecolor='black' ) plt.plot(ts, ys) plt.scatter([k["time"] for k in KEYFRAMES_RAW], [k["value"] for k in KEYFRAMES_RAW], label="Keyframes") plt.xlabel("Input") plt.ylabel("Output") plt.legend() plt.title("Quota randomizer curve") plt.grid(True, which='both', axis='both') plt.gca().xaxis.set_major_locator(MultipleLocator(0.05)) plt.gca().yaxis.set_major_locator(MultipleLocator(0.1)) ts_expect = np.linspace(t_min, t_max, 1000) ys_expect = np.array([curve.evaluate(t) for t in ts_expect]) expected_value = ys_expect.mean() print(f"期待値: {expected_value:.4f}") # 特別なX値でのY値を取得し、グラフに表示 special_xs = [0.7865, 1.012] for x in special_xs: y = curve.evaluate(x) plt.plot(x, y, 'ro') # 赤丸でマーク plt.text(x, y, f"({x:.4f}, {y:.4f})", color='red', fontsize=12, ha='right', va='bottom') # 制限値付きの期待値の計算 ts_expect = np.linspace(t_min, x, 1000) ys_expect = np.array([curve.evaluate(t) for t in ts_expect]) expected_value = ys_expect.mean() print(f"制限付き期待値(x <= {x}): {expected_value:.4f}") # Y=0となるtを探索し、同様にプロット zero_crossings: list[float] = [] for i in range(len(ts)-1): if ys[i] * ys[i+1] < 0: # 線形補間でゼロ点近似 t0, t1 = ts[i], ts[i+1] y0, y1 = ys[i], ys[i+1] t_zero = t0 - y0 * (t1 - t0) / (y1 - y0) zero_crossings.append(t_zero) for x in zero_crossings: y = 0.0 plt.plot(x, y, 'ro') plt.text(x, y, f"({x:.4f}, 0.0000)", color='red', fontsize=12, ha='right', va='bottom') plt.savefig("quota_randomizer_curve.png") if __name__ == "__main__": main() LuckとQuotaの関係 Luckが大きいほどQuotaは上がりにくくなり、Luckが小さいほどQuotaは上がりやすくなります。 ...

2025年8月13日 · aoirint

Lethal Company 家具のLuckと販売額の一覧 v72 (Build ID: 18916695)

Lethal Company v72 (Build ID: 18916695)の家具のLuckと販売額の一覧です。 販売額当たりのLuck効率が高い順にソートしています。 値は自作のLuckDumperでダンプしました。 QuotaとLuckの関係については、Quotaの計算を参照してください。 v72 (Build ID: 18916695) name luck cost luck/cost*10^5 Disco Ball 0.06 150 40.00 JackOLantern 0.012 50 24.00 Television 0.02 130 15.38 Electric chair 0.018 140 12.86 Microwave 0.01 80 12.50 Goldfish 0.006 50 12.00 Dog house 0.007 80 8.75 Shower 0.015 180 8.33 Welcome mat 0.003 40 7.50 Toilet 0.01 150 6.67 Table 0.004 70 5.71 Sofa chair 0.008 150 5.33 Fridge 0.01 225 4.44 Record player 0.005 120 4.17 Romantic table 0.005 120 4.17 Cozy lights 0.005 140 3.57 Plushie pajama man 0.003 100 3.00 Loud horn 0.0025 100 2.50 Classic painting 0.006 400 1.50 Inverse Teleporter 0.004 425 0.94 Green suit 0 60 0.00 Hazard suit 0 90 0.00 Pajama suit 0 900 0.00 Teleporter 0 375 0.00 Purple Suit 0 70 0.00 Bee Suit 0 110 0.00 Bunny Suit 0 200 0.00 Signal translator -0.012 255 -4.71

2025年8月9日 · aoirint

Lethal Company 家具のLuckと販売額の一覧 v68 (Build ID: 16274733)

Lethal Company v68 (Build ID: 16274733)の家具のLuckと販売額の一覧です。 販売額当たりのLuck効率が高い順にソートしています。 値は自作のLuckDumperでダンプしました。 QuotaとLuckの関係については、Quotaの計算を参照してください。 v68 (Build ID: 16274733) name luck cost luck/cost*10^5 Disco Ball 0.06 150 40.00 JackOLantern 0.012 50 24.00 Television 0.02 130 15.38 Goldfish 0.006 50 12.00 Shower 0.015 180 8.33 Welcome mat 0.003 40 7.50 Toilet 0.01 150 6.67 Table 0.004 70 5.71 Record player 0.005 120 4.17 Romantic table 0.005 120 4.17 Cozy lights 0.005 140 3.57 Plushie pajama man 0.003 100 3.00 Loud horn 0.0025 100 2.50 Inverse Teleporter 0.004 425 0.94 Green suit 0 60 0.00 Hazard suit 0 90 0.00 Pajama suit 0 900 0.00 Teleporter 0 375 0.00 Purple Suit 0 70 0.00 Bee Suit 0 110 0.00 Bunny Suit 0 200 0.00 Signal translator -0.012 255 -4.71

2025年8月9日 · aoirint

Lethal Company Mod Imperiumの使い方(Imperium v0.2.8)

Imperiumの導入 https://thunderstore.io/c/lethal-company/p/giosuel/Imperium/ やりたいこと メニュー(Imperium UI)を開きたい F1キーを押す。 または、Escメニューの「Settings」、「Change keybinds」から、「Imperium」のタブを開き、「Imperium UI」の項目に設定されているキーを押す。 敵のスポーンを無効化したい Imperiumメニューの中央部分、左から4番目の「Moon Control」の「Entity Spawning」欄にある、「Pause Indoor Spawning」、「Pause Outdoor Spawning」、「Pause Daytime Spawning」のチェックボックスをONにする。 時間経過を無効化したい Imperiumメニューの中央部分、左から4番目の「Moon Control」の「Time Settings」欄にある、「Pause Time」のチェックボックスをONにする。 時間経過を1分単位で表示したい Imperiumメニューの中央部分、左から4番目の「Moon Control」の「Time Settings」欄にある、「Realtime Clock」のチェックボックスをONにする。 バニラでは、HUD上部の時間はゲーム内時間で数分おきに更新される。この表示を1分単位で更新させることができる。 ダメージを無効化したい Imperiumメニューの一番左「Imperium Control Center」の「Player Settings」欄にある、「God Mode」のチェックボックスをONにする。 ダメージ通知を表示したい Imperiumメニューの一番右「Imperium Preferences」の「Notifications」欄にある、「God Mode」のチェックボックスをONにして、ゲームを再起動する。「Notifications」欄の設定変更は、Imperium v0.2.8時点では、ゲームを再起動しないと反映されないので注意。 この状態で「Imperium Control Center」の「God Mode」を有効化すると、ダメージ通知が表示される。 敵の体力や状態を表示したい Imperiumメニューの中央部分、一番左の「Visualization」の「Insights」欄にある、「Enemies」のチェックボックスをONにする。 敵の当たり判定を表示したい Imperiumメニューの中央部分、一番左の「Visualization」の「Coliders」欄にある、「Entities」のチェックボックスをONにする。 敵の動きを止めたい Imperiumメニューの左部分、左から2番目の「Object Explorer」の「Entities」欄にある、止めたい対象の項目で、右から3番目のチェックボックスをOFFにする。 だいたい移動は止まるが、例えばNutcrackerの場合、射撃体勢に入ってから止めても弾丸が発射されたり、蹴りは発生したりなど、完全に敵の行動を封じるわけではないので注意。 敵やアイテムをスポーンさせたい F2キーで表示される入力欄に敵やアイテムの名前を入力して、Enterで確定する。 敵やアイテムのスポーン位置を選択できる場合もあるが、選択なしで敵がその場にスポーンしたり、アイテムがアイテムスロットに入る場合もある。 敵の場合、プレイヤーの視界にスポーンさせられる場所があるか、アイテムの場合、クルーがアイテムを拾える状態かどうか、が影響していそうだが、詳細な条件は不明。 敵やアイテムを削除したい Imperiumメニューの左部分、左から2番目の「Object Explorer」から、対象の項目で、一番右のバツアイコンのボタンをクリックする。 ...

2025年1月12日 · aoirint