mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-07 19:25:00 +02:00
Compare commits
20 Commits
room
...
2026.7.0-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03d224acd0 | ||
|
|
c24ce9b55a | ||
|
|
7243bd34b9 | ||
|
|
d4dd02c744 | ||
|
|
3817f00803 | ||
|
|
f066c1c2ef | ||
|
|
1d41cb3d29 | ||
|
|
1858d56918 | ||
|
|
97082efd99 | ||
|
|
187610d516 | ||
|
|
00b2cb2076 | ||
|
|
426f6748e5 | ||
|
|
c423bf92a8 | ||
|
|
f59bf35615 | ||
|
|
0f056c4955 | ||
|
|
eb2c7ff6c6 | ||
|
|
9f614517c0 | ||
|
|
5432984af8 | ||
|
|
c29a3d902b | ||
|
|
721b1b06a0 |
@@ -303,6 +303,18 @@ id: 'aidx'
|
||||
|
||||
#sentryForBackend:
|
||||
# enableNodeProfiling: true
|
||||
# # Specify Sentry integration names to disable individual auto-instrumentation.
|
||||
# # The names are integration .name values, not factory function names.
|
||||
# # To check enabled names, set `options.debug: true` and see
|
||||
# # "Integration installed: <name>" in the Sentry logs.
|
||||
# #
|
||||
# # As of 2026-07-05 / @sentry/node 10.59.0, useful names for Misskey include:
|
||||
# # Postgres ... DB queries (when pg is externalized)
|
||||
# # Redis ... ioredis commands
|
||||
# # Fastify ... inbound HTTP routes
|
||||
# # Http ... inbound/outbound HTTP; disabling this can also affect request isolation
|
||||
# # NodeFetch ... fetch/undici requests
|
||||
# disabledIntegrations: ['Postgres']
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
|
||||
29
.github/workflows/lint.yml
vendored
29
.github/workflows/lint.yml
vendored
@@ -16,10 +16,10 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- scripts/check-dts*.mjs
|
||||
- .github/workflows/lint.yml
|
||||
- package.json
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/backend/**
|
||||
@@ -32,10 +32,10 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- scripts/check-dts*.mjs
|
||||
- .github/workflows/lint.yml
|
||||
- package.json
|
||||
jobs:
|
||||
pnpm_install:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -69,8 +69,6 @@ jobs:
|
||||
- misskey-js
|
||||
- misskey-bubble-game
|
||||
- misskey-reversi
|
||||
- misskey-world
|
||||
- frontend-misskey-world-engine
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
||||
@@ -119,3 +117,22 @@ jobs:
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter "${{ matrix.workspace }}^..." run build
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||
|
||||
check-dts:
|
||||
needs: [pnpm_install]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: node --test scripts/check-dts.test.mjs
|
||||
- run: pnpm check-dts
|
||||
|
||||
2
.github/workflows/report-api-diff.yml
vendored
2
.github/workflows/report-api-diff.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
echo '```diff' >> ./output.md
|
||||
cat ./api.json.diff >> ./output.md
|
||||
echo '```' >> ./output.md
|
||||
echo '</details>' >> ./output.md
|
||||
echo '</details>' >> .output.md
|
||||
fi
|
||||
|
||||
echo "$FOOTER" >> ./output.md
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## 2026.6.1
|
||||
## 2026.7.0
|
||||
|
||||
### Note
|
||||
|
||||
@@ -15,19 +15,24 @@
|
||||
|
||||
### General
|
||||
- Feat: コントロールパネルから二要素認証を解除できるように
|
||||
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
|
||||
(Based on https://github.com/MisskeyIO/misskey/pull/214)
|
||||
|
||||
### Client
|
||||
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
|
||||
- 2025.4.0 以前の設定情報の移行処理が削除されました
|
||||
- 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。
|
||||
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
|
||||
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
|
||||
- Fix: 自分へのメンションに対する色分けで、判定が大文字/小文字を区別していた問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804)
|
||||
- Enhance: Sentry バックエンドの自動計装を `sentryForBackend.disabledIntegrations` で個別に無効化できるように
|
||||
- Enhance: Node.js 22.23.0以降、24.17.0以降、26.4.0以降をサポートするように
|
||||
- Enhance: Docker Image の Node.js を 26.4.0 に、Debian を trixie (v13) に更新
|
||||
- Fix: `/stats` API のレスポンス型が正しくない問題を修正
|
||||
- Fix: ハッシュタグに関連するデータを更新する際のエラーハンドリングを修正
|
||||
- Fix: Sentry 使用環境下にて、Misskey が発行した SQL クエリが span に含まれない問題を修正
|
||||
|
||||
## 2026.6.0
|
||||
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -30,8 +30,6 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -63,8 +61,6 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -103,18 +99,12 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/node_modules ./packages/misskey-world/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/node_modules ./packages/frontend-misskey-world-engine/node_modules
|
||||
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/built ./packages/misskey-world/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/built ./packages/frontend-misskey-world-engine/built
|
||||
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so
|
||||
|
||||
@@ -1414,13 +1414,13 @@ viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
accessToken: "アクセストークン"
|
||||
choose: "選択"
|
||||
rotate: "回転"
|
||||
chooseEmojiPalette: "絵文字パレットを選択"
|
||||
addToEmojiPalette: "絵文字パレットに追加"
|
||||
emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?"
|
||||
append: "末尾に追加"
|
||||
prepend: "先頭に追加"
|
||||
urlPreviewSensitiveList: "サムネイルの表示を制限するURL"
|
||||
urlPreviewSensitiveListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルが表示されなくなります。"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -3580,476 +3580,3 @@ _qr:
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
||||
worldAvatar: "Worldアバター"
|
||||
worldAvatar_description: "MisskeyWorld / MisskeyRoomsで使用可能な3Dアバターを作成できます。"
|
||||
|
||||
_miWorld:
|
||||
separateRenderingThread: "描画を別スレッドに分離"
|
||||
separateRenderingThread_description: "有効にするとパフォーマンスが向上します。不安定になる場合は無効すると改善する可能性があります。"
|
||||
graphicsQuality: "グラフィックの品質"
|
||||
graphicsSettings: "グラフィック設定"
|
||||
frameRateLimitation: "フレームレート制限"
|
||||
higherValuePerformanceNote: "高くすると体験が向上しますが、消費電力が増加するなどパフォーマンスに影響を与えます。"
|
||||
resolution: "解像度"
|
||||
fov: "視野角"
|
||||
failedToInitialize: "初期化に失敗しました"
|
||||
crushed_description: "描画が継続できなくなりました。デバイスのリソース不足の可能性が考えられます。"
|
||||
antialiasing: "アンチエイリアス"
|
||||
avatar: "アバター"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
attachAccessory: "アクセサリーをつける"
|
||||
takeScreenShot: "スクリーンショット"
|
||||
onlineMenu: "オンラインメニュー"
|
||||
connectToOnline: "オンラインに接続"
|
||||
disconnectToOnline: "オンラインから切断"
|
||||
character: "キャラクター"
|
||||
sit: "座る"
|
||||
lyingDown: "寝そべる"
|
||||
standUp: "立ち上がる"
|
||||
showUsernameOnAvatar: "アバターにユーザー名を表示"
|
||||
show2dAvatarOnAvatar: "アバターにユーザーアイコンを表示"
|
||||
|
||||
_avatars:
|
||||
_default:
|
||||
body: "ボディ"
|
||||
eyes: "目"
|
||||
mouth: "口"
|
||||
|
||||
_avatarAccessories:
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
mikan: "みかん"
|
||||
bolt: "ボルト"
|
||||
_bolt:
|
||||
mat: "素材"
|
||||
|
||||
_miRoom:
|
||||
snapToGrid: "グリッドにスナップ"
|
||||
gridScale: "グリッドサイズ"
|
||||
thereAreUnsavedChanges: "未保存の変更があります"
|
||||
revertAllChangesConfirmation: "全ての変更を取り消し、部屋を最後に保存した状態まで戻しますか?"
|
||||
yourDeviceNotSupported_title: "MisskeyRoomを起動できません"
|
||||
yourDeviceNotSupported_description: "お使いのデバイスがMisskeyRoomをサポートしていないか、デバイスのリソース不足などにより一時的に利用できなくなっています。\nMisskeyRoomを動作させるには、WebGPUをサポートするデバイス・ブラウザが必要です。"
|
||||
imageFit: "画像のはめ込み"
|
||||
imageFit_cover: "覆う"
|
||||
imageFit_contain: "収める"
|
||||
imageFit_stretch: "伸縮"
|
||||
material_metallic: "光沢"
|
||||
material_roughness: "粗さ"
|
||||
light_brightness: "明るさ"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
enterEditMode: "エディットモードを始める"
|
||||
exitEditMode: "エディットモードを終了"
|
||||
installFurniture: "家具を設置"
|
||||
roomCustomize: "部屋のカスタマイズ"
|
||||
changeRoomName: "ルーム名を編集"
|
||||
roomInfo: "ルーム情報"
|
||||
duplicate: "複製"
|
||||
grab: "掴む"
|
||||
furnitureCustomize: "家具のアレンジ"
|
||||
uninstallFurniture: "しまう"
|
||||
furnituresCount: "家具の数"
|
||||
attachedFilesCount: "添付ファイル数"
|
||||
_furniturePlacement:
|
||||
top: "上面設置"
|
||||
bottom: "下面設置"
|
||||
side: "側面設置"
|
||||
|
||||
_furnitures:
|
||||
haniwa: "はにわ"
|
||||
_haniwa:
|
||||
bodyMat: "本体の素材"
|
||||
insideColor: "中身の色"
|
||||
woodRingFloorLamp: "リングシェードフロアランプ"
|
||||
_woodRingFloorLamp:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
a4Case: "A4ケース"
|
||||
_a4Case:
|
||||
mat: "素材"
|
||||
aircon: "エアコン"
|
||||
allInOnePc: "一体型PC"
|
||||
_allInOnePc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画面の画像"
|
||||
image_desktop: "デスクトップ"
|
||||
screenBrightness: "画面の明るさ"
|
||||
aquarium: "水槽"
|
||||
aromaReedDiffuser: "アロマリードディフューザー"
|
||||
_aromaReedDiffuser:
|
||||
bottleMat: "ボトルの素材"
|
||||
oilMat: "オイルの素材"
|
||||
banknote: "紙幣"
|
||||
beamLamp: "ビームランプ"
|
||||
bed: "ベッド"
|
||||
_bed:
|
||||
frameMat: "フレームの素材"
|
||||
blind: "ブラインド"
|
||||
_blind:
|
||||
angle: "羽根の回転角度"
|
||||
blades: "羽根の枚数"
|
||||
open: "開閉状態"
|
||||
book: "本"
|
||||
_book:
|
||||
height: "高さ"
|
||||
thickness: "厚み"
|
||||
variation: "バリエーション"
|
||||
width: "幅"
|
||||
books: "本の束"
|
||||
_books:
|
||||
variation: "バリエーション"
|
||||
boxWallShelf: "ボックス型ウォールシェルフ"
|
||||
_boxWallShelf:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
withBack: "背板"
|
||||
cactusS: "サボテン S"
|
||||
_cactusS:
|
||||
potMat: "鉢の素材"
|
||||
cardboardBox: "段ボール箱"
|
||||
_cardboardBox:
|
||||
variation: "種類"
|
||||
variation_aizon: "Aizon"
|
||||
variation_default: "デフォルト"
|
||||
variation_mikan: "みかん"
|
||||
ceilingFanLight: "シーリングファンライト"
|
||||
_ceilingFanLight:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
ceilingFan: "シーリングファン"
|
||||
_ceilingFan:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
chair: "椅子"
|
||||
_chair:
|
||||
primaryMat: "メインの素材"
|
||||
secondaryMat: "サブの素材"
|
||||
frameMat: "フレームの素材"
|
||||
clippedPicture: "留められた写真"
|
||||
_clippedPicture:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
coffeeCup: "コーヒーカップ"
|
||||
colorBox: "カラーボックス"
|
||||
_colorBox:
|
||||
mat: "素材"
|
||||
cuboid: "直方体"
|
||||
_cuboid:
|
||||
mat: "素材"
|
||||
x: "X"
|
||||
y: "Y"
|
||||
z: "Z"
|
||||
cupNoodle: "インスタントラーメン"
|
||||
curtain: "カーテン"
|
||||
custardPudding: "プリン"
|
||||
descriptionPlate: "説明が書かれたプレート"
|
||||
desk: "デスク"
|
||||
_desk:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
width: "幅"
|
||||
desktopPc: "デスクトップPC"
|
||||
_desktopPc:
|
||||
bodyMat: "本体の素材"
|
||||
coverMat: "カバーの素材"
|
||||
inner1Mat: "内部素材1"
|
||||
inner2Mat: "内部素材2"
|
||||
inner3Mat: "内部素材3"
|
||||
ledColor: "LEDの色"
|
||||
djMixer: "DJミキサー"
|
||||
djPlayer: "DJプレーヤー"
|
||||
_djPlayer:
|
||||
image: "画像"
|
||||
"image:waveform": "波形"
|
||||
screenBrightness: "画面の明るさ"
|
||||
ductRailSpotLights: "スポットライト付きダクトレール"
|
||||
_ductRailSpotLights:
|
||||
angleH: "水平角度"
|
||||
angleV: "垂直角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
ductTape: "ガムテープ"
|
||||
herbarium: "ハーバリウム"
|
||||
electronicDisplayBoard: "電光掲示板"
|
||||
_electronicDisplayBoard:
|
||||
frameMat: "フレームの素材"
|
||||
ledBrightness: "LEDの明るさ"
|
||||
ledColor: "LEDの色"
|
||||
text: "テキスト"
|
||||
emptyBento: "空の弁当容器"
|
||||
energyDrink: "エナジードリンク"
|
||||
envelope: "封筒"
|
||||
facialTissue: "ティッシュ"
|
||||
glassCylinderPotPlant: "ガラスシリンダーの鉢植えと植物"
|
||||
handheldGameConsole: "携帯ゲーム機"
|
||||
_handheldGameConsole:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
hangingDuctRail: "吊り下げダクトレール"
|
||||
_hangingDuctRail:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
hangingTShirt: "吊り下げTシャツ"
|
||||
icosahedron: "正二十面体のオブジェ"
|
||||
_icosahedron:
|
||||
mat: "素材"
|
||||
ironFrameTable: "アイアンフレームテーブル"
|
||||
_ironFrameTable:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
issyoubin: "一升瓶"
|
||||
_issyoubin:
|
||||
variation: "種類"
|
||||
keyboard: "キーボード"
|
||||
_keyboard:
|
||||
bodyMat: "本体の素材"
|
||||
keyMat: "キーの素材"
|
||||
laptopPc: "ノートPC"
|
||||
_laptopPc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
openAngle: "開き具合"
|
||||
screenBrightness: "画面の明るさ"
|
||||
largeMousepad: "大きいマウスパッド"
|
||||
_largeMousepad:
|
||||
image: "画像"
|
||||
lavaLamp: "ラバランプ"
|
||||
_lavaLamp:
|
||||
bodyMat: "本体の素材"
|
||||
glassMat: "ガラスの素材"
|
||||
lavaColor: "オイルの色"
|
||||
lightColor: "ランプの色"
|
||||
letterCase: "レターケース"
|
||||
lowPartitionBar: "低いパーティションバー"
|
||||
_lowPartitionBar:
|
||||
bodyMat: "本体の素材"
|
||||
width: "幅"
|
||||
miObjet: "Miオブジェ"
|
||||
miPlate: "Miプレート"
|
||||
miPlateDisplayed: "飾られたMiプレート"
|
||||
milk: "牛乳"
|
||||
mixer: "ミキサー"
|
||||
monitor: "モニター"
|
||||
_monitor:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
monitorSpeaker: "モニタースピーカー"
|
||||
_monitorSpeaker:
|
||||
mat: "素材"
|
||||
monstera: "モンステラ"
|
||||
_monstera:
|
||||
potMat: "鉢の素材"
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
newtonsCradle: "ニュートンクレードル"
|
||||
_newtonsCradle:
|
||||
frameMat: "フレームの素材"
|
||||
openedCardboardBox: "開いた段ボール箱"
|
||||
pachira: "パキラ"
|
||||
_pachira:
|
||||
potMat: "鉢の素材"
|
||||
petBottle: "ペットボトル"
|
||||
_petBottle:
|
||||
empty: "空"
|
||||
variation: "種類"
|
||||
variation_greenTea: "緑茶"
|
||||
variation_mineralWater: "ミネラルウォーター"
|
||||
withCap: "キャップ"
|
||||
withLabel: "ラベル"
|
||||
piano: "ピアノ"
|
||||
_piano:
|
||||
bodyMat: "本体の素材"
|
||||
pictureFrame: "シンプルな額縁"
|
||||
_pictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
withCover: "カバーあり"
|
||||
pizza: "ピザ"
|
||||
plant: "植物"
|
||||
plant2: "植物 2"
|
||||
poster: "ポスター"
|
||||
_poster:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
powerStrip: "電源タップ"
|
||||
radiometer: "ラジオメーター"
|
||||
randomBooks: "雑多な本"
|
||||
_randomBooks:
|
||||
count: "数"
|
||||
seed: "シード"
|
||||
stackVertically: "平積み"
|
||||
variation: "バリエーション"
|
||||
variation_mix: "いろいろ"
|
||||
variation_mixPlain: "いろいろ(無地)"
|
||||
recordPlayer: "レコードプレーヤー"
|
||||
rolledUpPoster: "丸めたポスター"
|
||||
roundRug: "円形のラグ"
|
||||
router: "ルーター"
|
||||
siphon: "サイフォン"
|
||||
snakeplant: "サンセベリア"
|
||||
_snakeplant:
|
||||
potMat: "鉢の素材"
|
||||
sofa: "ソファ"
|
||||
_sofa:
|
||||
bodyMat: "本体の素材"
|
||||
speaker: "スピーカー"
|
||||
_speaker:
|
||||
innerMat: "内側の素材"
|
||||
outerMat: "外側の素材"
|
||||
speakerStand: "スピーカースタンド"
|
||||
_speakerStand:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
spotLight: "スポットライト"
|
||||
_spotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
downlight: "ダウンライト"
|
||||
_downlight:
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
sprayer: "霧吹き"
|
||||
stanchionPole: "スタンションポール"
|
||||
_stanchionPole:
|
||||
bodyMat: "本体の素材"
|
||||
ropeMat: "ロープの素材"
|
||||
steelRack: "スチールラック"
|
||||
_steelRack:
|
||||
height: "高さ"
|
||||
numberOfShelfs: "シェルフの数"
|
||||
poleMat: "ポールの素材"
|
||||
shelfPositionOf: "シェルフの位置"
|
||||
shelfMat: "シェルフの素材"
|
||||
widthAndDepthVariation: "W x D"
|
||||
stormGlass: "ストームグラス"
|
||||
tableSalt: "食卓塩"
|
||||
tabletopCalendar: "卓上カレンダー"
|
||||
tabletopDigitalClock: "卓上デジタル時計"
|
||||
_tabletopDigitalClock:
|
||||
bodyMat: "本体の素材"
|
||||
lcdColor: "LCDの色"
|
||||
tabletopFlag: "卓上旗"
|
||||
_tabletopFlag:
|
||||
image: "画像"
|
||||
tabletopGlassPictureFrame: "卓上ガラス製フォトフレーム"
|
||||
_tabletopGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tabletopIronFrameStand: "卓上アイアンフレームスタンド"
|
||||
_tabletopIronFrameStand:
|
||||
boardMat: "板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
tabletopLcdButtonsController: "LCDボタン付き卓上コントローラー"
|
||||
_tabletopLcdButtonsController:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
tabletopPictureFrame: "卓上フォトフレーム"
|
||||
_tabletopPictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
tapestry: "タペストリー"
|
||||
_tapestry:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tetrapod: "波消ブロックの置物"
|
||||
tv: "テレビ"
|
||||
_tv:
|
||||
bodyMat: "本体の素材"
|
||||
screenBrightness: "画面の明るさ"
|
||||
twistedCubeObjet: "ねじれた立方体のオブジェ"
|
||||
usedTissue: "使用済みティッシュ"
|
||||
wallCanvas: "壁掛けキャンバス"
|
||||
_wallCanvas:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallClock: "壁掛け時計"
|
||||
_wallClock:
|
||||
frameMat: "フレームの素材"
|
||||
faceMat: "文字盤の素材"
|
||||
handsMat: "針の素材"
|
||||
wallGlassPictureFrame: "ガラスの壁掛けフォトフレーム"
|
||||
_wallGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallMirror: "壁掛けミラー"
|
||||
_wallMirror:
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの厚み"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
wallMountSpotLight: "ウォールマウントスポットライト"
|
||||
_wallMountSpotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
wallShelf: "ウォールシェルフ"
|
||||
_wallShelf:
|
||||
boardMat: "板の素材"
|
||||
boardStyle: "板のスタイル"
|
||||
"boardStyle:color": "単色"
|
||||
"boardStyle:wood": "木目"
|
||||
style: "スタイル"
|
||||
wireBasket: "ワイヤーバスケット"
|
||||
_wireBasket:
|
||||
bodyMat: "本体の素材"
|
||||
wireNet: "ワイヤーネット"
|
||||
_wireNet:
|
||||
bodyMat: "本体の素材"
|
||||
woodRingsPendantLight: "リングペンダントライト"
|
||||
_woodRingsPendantLight:
|
||||
bodyMat: "本体の素材"
|
||||
length: "長さ"
|
||||
light: "照明"
|
||||
shadeMat: "シェードの素材"
|
||||
woodSoundAbsorbingPanel: "木製吸音パネル"
|
||||
ironFrameShelf: "アイアンフレームシェルフ"
|
||||
_ironFrameShelf:
|
||||
boardMat: "板の素材"
|
||||
frameMat: "フレームの素材"
|
||||
height: "段数"
|
||||
width: "幅"
|
||||
kakejiku: "掛軸"
|
||||
_kakejiku:
|
||||
image: "画像"
|
||||
"image:kitsuaishinsei": "喫藍心整"
|
||||
|
||||
26
package.json
26
package.json
@@ -1,20 +1,18 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.6.1-alpha.1",
|
||||
"version": "2026.7.0-alpha.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@11.8.0",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/misskey-world",
|
||||
"packages/icons-subsetter",
|
||||
"packages/frontend-misskey-world-engine",
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend-builder",
|
||||
"packages/sw",
|
||||
@@ -41,7 +39,8 @@
|
||||
"migrateandstart": "pnpm migrate && pnpm start",
|
||||
"watch": "pnpm dev",
|
||||
"dev": "node scripts/dev.mjs",
|
||||
"lint": "pnpm --no-bail -r lint",
|
||||
"check-dts": "node scripts/check-dts.mjs",
|
||||
"lint": "pnpm --no-bail -r lint && pnpm check-dts",
|
||||
"cy:open": "pnpm cypress open --config-file=cypress.config.ts",
|
||||
"cy:run": "pnpm cypress run",
|
||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
@@ -58,23 +57,22 @@
|
||||
"esbuild": "0.28.1",
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "9.0.0",
|
||||
"js-yaml": "4.2.0",
|
||||
"tar": "7.5.16"
|
||||
"js-yaml": "5.2.0",
|
||||
"tar": "7.5.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.4",
|
||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "26.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@types/node": "26.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260426.1",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.17.0",
|
||||
"cypress": "15.18.0",
|
||||
"eslint": "9.39.4",
|
||||
"globals": "17.6.0",
|
||||
"globals": "17.7.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "11.8.0",
|
||||
"pnpm": "11.9.0",
|
||||
"start-server-and-test": "3.0.11",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldRoom1778744540138 {
|
||||
name = 'WorldRoom1778744540138'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_room" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "likedCount" integer NOT NULL DEFAULT '0', "visibility" character varying(128) NOT NULL DEFAULT 'public', "def" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_40cfacaf35b0b54bb2281c89767" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_88289375952050da4a7752a366" ON "world_room" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f803a5efb4125c5fd8a414285e" ON "world_room" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f803a5efb4125c5fd8a414285e"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_88289375952050da4a7752a366"`);
|
||||
await queryRunner.query(`DROP TABLE "world_room"`);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldAvatar1779921322355 {
|
||||
name = 'WorldAvatar1779921322355'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_avatar" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "def" jsonb NOT NULL DEFAULT '{}', "active" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_e7a27262285cc2c27114871f866" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0f1d0bdfaca455cc2f13defabe" ON "world_avatar" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4eba43c8e2540a92e99dd7f5a9" ON "world_avatar" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD "accessCount" integer NOT NULL DEFAULT '0'`);
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" ADD CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" DROP CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a"`);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP COLUMN "accessCount"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4eba43c8e2540a92e99dd7f5a9"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0f1d0bdfaca455cc2f13defabe"`);
|
||||
await queryRunner.query(`DROP TABLE "world_avatar"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class UrlPreviewSensitiveList1782581064131 {
|
||||
name = 'UrlPreviewSensitiveList1782581064131'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewSensitiveList" character varying(3072) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewSensitiveList"`);
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,8 @@
|
||||
"utf-8-validate": "6.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1073.0",
|
||||
"@aws-sdk/lib-storage": "3.1073.0",
|
||||
"@aws-sdk/client-s3": "3.1075.0",
|
||||
"@aws-sdk/lib-storage": "3.1075.0",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/http-proxy": "11.5.0",
|
||||
@@ -63,23 +63,22 @@
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
"@misskey-dev/sharp-read-bmp": "1.3.1",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@napi-rs/canvas": "1.0.0",
|
||||
"@napi-rs/canvas": "1.0.1",
|
||||
"@nestjs/common": "11.1.27",
|
||||
"@nestjs/core": "11.1.27",
|
||||
"@nestjs/testing": "11.1.27",
|
||||
"@oxc-project/runtime": "0.137.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.59.0",
|
||||
"@sentry/profiling-node": "10.59.0",
|
||||
"@simplewebauthn/server": "13.3.1",
|
||||
"@sinonjs/fake-timers": "15.4.0",
|
||||
"@smithy/node-http-handler": "4.8.1",
|
||||
"@sentry/node": "10.62.0",
|
||||
"@sentry/profiling-node": "10.62.0",
|
||||
"@simplewebauthn/server": "13.3.2",
|
||||
"@smithy/node-http-handler": "4.8.2",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.20.0",
|
||||
"archiver": "8.0.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bullmq": "5.79.0",
|
||||
"bullmq": "5.79.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
@@ -88,12 +87,12 @@
|
||||
"content-disposition": "2.0.1",
|
||||
"date-fns": "4.4.0",
|
||||
"deep-email-validator": "0.1.27",
|
||||
"fastify": "5.8.5",
|
||||
"fastify": "5.9.0",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "5.2.1",
|
||||
"file-type": "22.0.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"got": "15.0.5",
|
||||
"got": "15.0.7",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
@@ -110,10 +109,10 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.14",
|
||||
"nanoid": "5.1.16",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.1.0",
|
||||
"node-html-parser": "8.0.3",
|
||||
"nodemailer": "9.0.1",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.5.1",
|
||||
@@ -135,7 +134,7 @@
|
||||
"slacc": "0.1.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.31.7",
|
||||
"systeminformation": "5.31.11",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.7",
|
||||
"tsc-alias": "1.8.17",
|
||||
@@ -150,16 +149,16 @@
|
||||
"@kitajs/ts-html-plugin": "4.1.4",
|
||||
"@nestjs/platform-express": "11.1.27",
|
||||
"@rollup/plugin-esm-shim": "0.1.8",
|
||||
"@sentry/vue": "10.59.0",
|
||||
"@sentry/vue": "10.62.0",
|
||||
"@sinonjs/fake-timers": "15.4.0",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "8.0.0",
|
||||
"@types/fluent-ffmpeg": "2.1.28",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "26.0.0",
|
||||
"@types/node": "26.0.1",
|
||||
"@types/nodemailer": "8.0.1",
|
||||
"@types/pg": "8.20.0",
|
||||
"@types/qrcode": "1.5.6",
|
||||
@@ -170,14 +169,13 @@
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/simple-oauth2": "5.0.8",
|
||||
"@types/sinonjs__fake-timers": "15.0.1",
|
||||
"@types/supertest": "7.2.0",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor2": "2.3.0",
|
||||
@@ -185,11 +183,10 @@
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "9.6.1",
|
||||
"fkill": "10.0.3",
|
||||
"js-yaml": "4.2.0",
|
||||
"js-yaml": "5.2.0",
|
||||
"pid-port": "2.1.1",
|
||||
"rolldown": "1.1.2",
|
||||
"rolldown": "1.1.3",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.2.2",
|
||||
"vite": "8.1.0",
|
||||
"vitest": "4.1.9",
|
||||
"vitest-mock-extended": "4.0.0"
|
||||
|
||||
@@ -75,6 +75,8 @@ export default defineConfig((args) => {
|
||||
're2',
|
||||
'ipaddr.js',
|
||||
'file-type',
|
||||
// バンドルするとSentryの自動計装が正しく行われなくなるため外しておく
|
||||
'pg',
|
||||
];
|
||||
|
||||
const define: Record<string, string> = {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import fs from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import yaml from 'js-yaml';
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -34,7 +34,7 @@ function yamlToJson(ymlPath) {
|
||||
console.log(`${ymlPath} → ${OUTPUT_PATH}`);
|
||||
|
||||
const yamlContent = fs.readFileSync(ymlPath, 'utf-8');
|
||||
const jsonContent = yaml.load(yamlContent);
|
||||
const jsonContent = loadYaml(yamlContent);
|
||||
if (!fs.existsSync(dirname(OUTPUT_PATH))) {
|
||||
fs.mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ declare module 'probe-image-size' {
|
||||
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
|
||||
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
|
||||
|
||||
namespace probeImageSize {} // Hack
|
||||
|
||||
export = probeImageSize;
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { probeImageSize as default };
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { initTelemetry } from '@/core/telemetry/telemetry-registry.js';
|
||||
import { initExtraThreadPool, jobQueue, server } from './common.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
@@ -66,26 +67,7 @@ export async function masterMain() {
|
||||
|
||||
initExtraThreadPool(config);
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
await initTelemetry(config);
|
||||
|
||||
bootLogger.info(
|
||||
`mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import cluster from 'node:cluster';
|
||||
import { envOption } from '@/env.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { initTelemetry } from '@/core/telemetry/telemetry-registry.js';
|
||||
import { initExtraThreadPool, jobQueue, server } from './common.js';
|
||||
|
||||
/**
|
||||
@@ -16,26 +17,7 @@ export async function workerMain() {
|
||||
|
||||
initExtraThreadPool(config);
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
],
|
||||
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.sentryForBackend.options,
|
||||
});
|
||||
}
|
||||
await initTelemetry(config);
|
||||
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
|
||||
@@ -20,6 +20,12 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
type SentryBackendConfig = {
|
||||
options: Partial<Sentry.NodeOptions>;
|
||||
enableNodeProfiling: boolean;
|
||||
disabledIntegrations?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 設定ファイルの型
|
||||
*/
|
||||
@@ -64,7 +70,7 @@ type Source = {
|
||||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
||||
sentryForBackend?: SentryBackendConfig;
|
||||
sentryForFrontend?: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
@@ -201,7 +207,7 @@ export type Config = {
|
||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||
sentryForBackend: SentryBackendConfig | undefined;
|
||||
sentryForFrontend: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
|
||||
@@ -154,15 +154,12 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
|
||||
import { QueueModule } from './QueueModule.js';
|
||||
import { QueueService } from './QueueService.js';
|
||||
import { LoggerService } from './LoggerService.js';
|
||||
import { WorldRoomService } from './WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js';
|
||||
import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js';
|
||||
import { WorldAvatarService } from './WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js';
|
||||
import { TelemetryService } from './telemetry/TelemetryService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
||||
const $TelemetryService: Provider = { provide: 'TelemetryService', useExisting: TelemetryService };
|
||||
const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService };
|
||||
const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService };
|
||||
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||
@@ -234,9 +231,6 @@ const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatServic
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
|
||||
const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService };
|
||||
const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService };
|
||||
const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -292,8 +286,6 @@ const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||
const $WorldRoomEntityService: Provider = { provide: 'WorldRoomEntityService', useExisting: WorldRoomEntityService };
|
||||
const $WorldAvatarEntityService: Provider = { provide: 'WorldAvatarEntityService', useExisting: WorldAvatarEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -392,9 +384,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
@@ -450,8 +439,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -473,6 +460,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ApPersonService,
|
||||
ApQuestionService,
|
||||
QueueService,
|
||||
TelemetryService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
@@ -547,9 +535,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
@@ -605,8 +590,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -626,6 +609,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ApNoteService,
|
||||
$ApPersonService,
|
||||
$ApQuestionService,
|
||||
$TelemetryService,
|
||||
//#endregion
|
||||
],
|
||||
exports: [
|
||||
@@ -702,9 +686,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -759,8 +740,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -782,6 +761,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ApPersonService,
|
||||
ApQuestionService,
|
||||
QueueService,
|
||||
TelemetryService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
$LoggerService,
|
||||
@@ -855,9 +835,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -912,8 +889,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -933,6 +908,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ApNoteService,
|
||||
$ApPersonService,
|
||||
$ApQuestionService,
|
||||
$TelemetryService,
|
||||
//#endregion
|
||||
],
|
||||
})
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import juice from 'juice';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { validate as validateEmail } from 'deep-email-validator';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -197,6 +195,7 @@ export class EmailService {
|
||||
} else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) {
|
||||
validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey);
|
||||
} else {
|
||||
const { validate: validateEmail } = await import('deep-email-validator');
|
||||
validated = await validateEmail({
|
||||
email: emailAddress,
|
||||
validateRegex: true,
|
||||
|
||||
@@ -173,16 +173,6 @@ export interface ChatEventTypes {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorldRoomEventTypes {
|
||||
enter: {
|
||||
user: Packed<'UserLite'>;
|
||||
avatar: Packed<'WorldAvatarLite'>['def'] | null;
|
||||
};
|
||||
left: {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
@@ -325,10 +315,6 @@ export type GlobalEvents = {
|
||||
name: `chatRoomStream:${MiChatRoom['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
worldRoom: {
|
||||
name: `worldRoomStream:${string}`;
|
||||
payload: EventTypesToEventPayload<WorldRoomEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
@@ -449,9 +435,4 @@ export class GlobalEventService {
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishWorldRoomStream<K extends keyof WorldRoomEventTypes>(roomId: string, type: K, value?: WorldRoomEventTypes[K]): void {
|
||||
this.publish(`worldRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldAvatar,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
public defaultAvatar = {
|
||||
type: 'default',
|
||||
body: {
|
||||
color: [0.8, 0.8, 0.8],
|
||||
roughness: 1,
|
||||
metallic: 0,
|
||||
},
|
||||
eyes: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
mouth: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
accessories: [],
|
||||
} satisfies MiWorldAvatar['def'];
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldAvatar['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyAvatarById(userId: MiUser['id'], avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ id: avatarId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findAvatarById(avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOne({ where: { id: avatarId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMyAvatarsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiWorldAvatar['id'] | null, untilId?: MiWorldAvatar['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldAvatarsRepository.createQueryBuilder('avatar'), sinceId, untilId)
|
||||
.andWhere('avatar.userId = :userId', { userId });
|
||||
|
||||
const avatars = await query.take(limit).getMany();
|
||||
|
||||
return avatars;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<MiWorldAvatar> {
|
||||
const currentAvatarsCount = await this.worldAvatarsRepository.countBy({ userId: me.id });
|
||||
|
||||
// TODO: limit by role policy
|
||||
|
||||
const avatar = await this.worldAvatarsRepository.insertOne(new MiWorldAvatar({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
active: currentAvatarsCount === 0,
|
||||
}));
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
avatar: MiWorldAvatar,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
const updated = await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: avatar.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
|
||||
if (body.active) {
|
||||
await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set({ active: false })
|
||||
.where('userId = :userId', { userId: avatar.userId })
|
||||
.andWhere('id != :id', { id: avatar.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(avatar: MiWorldAvatar, deleter?: MiUser): Promise<void> {
|
||||
await this.worldAvatarsRepository.delete(avatar.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUser(userId: MiUser['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ userId, active: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUsers(userIds: MiUser['id'][]): Promise<MiWorldAvatar[]> {
|
||||
if (userIds.length === 0) return [];
|
||||
return this.worldAvatarsRepository.findBy({ userId: In(userIds), active: true });
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { MiWorldAvatar, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
type PlayerState = {
|
||||
position: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
sit?: string; // id
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomMultiplayService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userEntityService: UserEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async enter(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('enter', { userId, roomId });
|
||||
|
||||
// TODO: atomicにやる
|
||||
const currentPlayers = await this.redisClient.hlen(`worldRoom:${roomId}:players`);
|
||||
if (currentPlayers < 10) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:players`, userId, 1);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Room is full.');
|
||||
}
|
||||
|
||||
// TODO: 既に入っていたらスキップ
|
||||
const avatar = await this.worldAvatarService.getActiveAvatarOfUser(userId);
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'enter', {
|
||||
user: await this.userEntityService.pack(userId),
|
||||
avatar: avatar?.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async heartbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
const exists = await this.redisClient.hexists(`worldRoom:${roomId}:players`, userId);
|
||||
if (exists) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Not in room.');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async left(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('left', { userId, roomId });
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:players`, userId);
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:playerStates`, userId);
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'left', {
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updatePlayerState(userId: MiUser['id'], roomId: MiWorldRoom['id'], state: PlayerState) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:playerStates`, userId, JSON.stringify(state));
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerStates(roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
const entries = await this.redisClient.hgetall(`worldRoom:${roomId}:playerStates`);
|
||||
return Object.fromEntries(Object.entries(entries).map(([userId, state]) => [userId, JSON.parse(state) as PlayerState]));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPlayerStatesAndHeatbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
// TODO: atomicにやる
|
||||
this.heartbeat(userId, roomId);
|
||||
return this.getPlayerStates(roomId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packPlayerProfile(user: Packed<'UserLite'>, avatar: Packed<'WorldAvatarLite'>['def'] | null) {
|
||||
return {
|
||||
user: {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl,
|
||||
},
|
||||
avatar: avatar ?? this.worldAvatarService.defaultAvatar,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerProfiles(roomId: MiWorldRoom['id'], userId?: MiUser['id']): Promise<Record<string, any>> {
|
||||
let playerIds = await this.redisClient.hkeys(`worldRoom:${roomId}:players`);
|
||||
playerIds = playerIds.filter(id => id !== userId);
|
||||
|
||||
const packedUsers = await this.userEntityService.packMany(playerIds);
|
||||
const avatars = await this.worldAvatarService.getActiveAvatarOfUsers(playerIds);
|
||||
|
||||
const profiles: Record<string, any> = {};
|
||||
for (const playerId of playerIds) {
|
||||
const packedUser = packedUsers.find(u => u.id === playerId);
|
||||
if (packedUser == null) continue;
|
||||
profiles[playerId] = this.packPlayerProfile(packedUser, avatars.find(a => a.userId === playerId)?.def ?? null);
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
const driveFileReferencingOptions = {
|
||||
clippedPicture: ['image'],
|
||||
tapestry: ['image'],
|
||||
poster: ['image'],
|
||||
pictureFrame: ['image'],
|
||||
tabletopPictureFrame: ['image'],
|
||||
tabletopGlassPictureFrame: ['image'],
|
||||
wallCanvas: ['image'],
|
||||
wallGlassPictureFrame: ['image'],
|
||||
tabletopFlag: ['image'],
|
||||
tabletopLcdButtonsController: ['image'],
|
||||
djPlayer: ['image'],
|
||||
monitor: ['image'],
|
||||
allInOnePc: ['image'],
|
||||
laptopPc: ['image'],
|
||||
handheldGameConsole: ['image'],
|
||||
largeMousepad: ['image'],
|
||||
kakejiku: ['image'],
|
||||
} as Record<string, string[]>;
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldRoom['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO: スキーマ検証(関係ないプロパティを入れたり不正な値を入れたりできないように)
|
||||
// そのためにはJSON SchemaでRoomState/各objectのoptionsを定義する必要がある
|
||||
|
||||
const objectsLimit = 100; // TODO: ref role policy
|
||||
if (def.installedFurnitures.length > objectsLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFilesLimit = 30; // TODO: ref role policy
|
||||
|
||||
const attachedFileIds = this.collectReferencedDriveFileIds(def);
|
||||
if (attachedFileIds.size > attachedFilesLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: me.id });
|
||||
for (const file of attachedFiles) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return false;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
if (Math.max(file.properties.width ?? 0, file.properties.height ?? 0) > 2048) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyRoomById(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOneBy({ id: roomId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findRoomById(roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOne({ where: { id: roomId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoomsOfUserWithPagination(userId: MiUser['id'], self: boolean, limit: number, sinceId?: MiWorldRoom['id'] | null, untilId?: MiWorldRoom['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
|
||||
.andWhere('room.userId = :userId', { userId });
|
||||
|
||||
if (!self) {
|
||||
query.andWhere('room.visibility = :visibility', { visibility: 'public' });
|
||||
}
|
||||
|
||||
const rooms = await query.take(limit).getMany();
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<MiWorldRoom> {
|
||||
const room = await this.worldRoomsRepository.insertOne(new MiWorldRoom({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
visibility: body.visibility,
|
||||
}));
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
room: MiWorldRoom,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
return this.worldRoomsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: room.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(room: MiWorldRoom, deleter?: MiUser): Promise<void> {
|
||||
await this.worldRoomsRepository.delete(room.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public collectReferencedDriveFileIds(roomState: MiWorldRoom['def']): Set<MiDriveFile['id']> {
|
||||
const fileIds = new Set<MiDriveFile['id']>();
|
||||
const installedFurnitures = roomState.installedFurnitures ?? roomState.installedObjects; // 後方互換性のため
|
||||
for (const o of installedFurnitures) {
|
||||
const def = driveFileReferencingOptions[o.type];
|
||||
if (def == null) continue;
|
||||
for (const key of def) {
|
||||
const optionValue = o.options[key];
|
||||
if (optionValue != null && optionValue.driveFileId != null && optionValue.driveFileId !== '') {
|
||||
fileIds.add(optionValue.driveFileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileIds;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldAvatar, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
def: avatar.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
createdAt: this.idService.parse(avatar.id).date.toISOString(),
|
||||
updatedAt: avatar.updatedAt.toISOString(),
|
||||
name: avatar.name,
|
||||
def: avatar.def,
|
||||
active: avatar.active,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packLite(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailedMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packDetailed(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldRoom, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const attachedFileIds = this.worldRoomService.collectReferencedDriveFileIds(room.def);
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: room.userId });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
def: room.def,
|
||||
attachedFiles: this.driveFileEntityService.packMany(attachedFiles),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
rooms: MiWorldRoom[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = rooms.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(rooms.map(room => this.packLite(room, me, { packedUser: _userMap.get(room.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/backend/src/core/telemetry/TelemetryService.ts
Normal file
28
packages/backend/src/core/telemetry/TelemetryService.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { captureMessage, shutdownTelemetry, startSpan } from './telemetry-registry.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
|
||||
@Injectable()
|
||||
export class TelemetryService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
captureMessage(message, opts);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public startSpan<T>(name: string, fn: () => T): T {
|
||||
return startSpan(name, fn);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async onApplicationShutdown(_signal?: string): Promise<void> {
|
||||
await shutdownTelemetry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js';
|
||||
|
||||
type SentryIntegrationsOption = NonNullable<import('@sentry/node').NodeOptions['integrations']>;
|
||||
type SentryIntegrationFactory = Extract<SentryIntegrationsOption, (integrations: any[]) => any[]>;
|
||||
type SentryIntegration = Parameters<SentryIntegrationFactory>[0][number];
|
||||
|
||||
type BuildSentryIntegrationsOptions = {
|
||||
disabledIntegrations?: string[];
|
||||
enableNodeProfiling: boolean;
|
||||
nodeProfilingIntegration?: () => SentryIntegration;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export function buildSentryIntegrations(options: BuildSentryIntegrationsOptions): SentryIntegrationFactory {
|
||||
return (defaults) => {
|
||||
const disabledIntegrations = new Set(options.disabledIntegrations ?? []);
|
||||
const defaultIntegrationNames = new Set(defaults.map((integration) => integration.name));
|
||||
const unknownIntegrations = [...disabledIntegrations].filter((name) => !defaultIntegrationNames.has(name));
|
||||
|
||||
if (unknownIntegrations.length > 0) {
|
||||
(options.warn ?? console.warn)(`Unknown Sentry integration configured in sentryForBackend.disabledIntegrations: ${unknownIntegrations.join(', ')}`);
|
||||
}
|
||||
|
||||
return [
|
||||
...defaults.filter((integration) => !disabledIntegrations.has(integration.name)),
|
||||
...(options.enableNodeProfiling && options.nodeProfilingIntegration != null ? [options.nodeProfilingIntegration()] : []),
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export class SentryTelemetryAdapter implements TelemetryAdapter {
|
||||
private constructor(
|
||||
private readonly Sentry: typeof import('@sentry/node'),
|
||||
) {
|
||||
}
|
||||
|
||||
public static async create(config: NonNullable<Config['sentryForBackend']>): Promise<SentryTelemetryAdapter> {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
||||
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
|
||||
maxBreadcrumbs: 0,
|
||||
|
||||
...config.options,
|
||||
|
||||
integrations: buildSentryIntegrations({
|
||||
disabledIntegrations: config.disabledIntegrations,
|
||||
enableNodeProfiling: config.enableNodeProfiling,
|
||||
nodeProfilingIntegration,
|
||||
}),
|
||||
});
|
||||
|
||||
return new SentryTelemetryAdapter(Sentry);
|
||||
}
|
||||
|
||||
public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
this.Sentry.captureMessage(message, {
|
||||
level: opts.level,
|
||||
...(opts.userId != null ? { user: { id: opts.userId } } : {}),
|
||||
extra: opts.extra,
|
||||
});
|
||||
}
|
||||
|
||||
public startSpan<T>(name: string, fn: () => T): T {
|
||||
return this.Sentry.startSpan({ name }, fn);
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
await this.Sentry.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface TelemetryCaptureMessageOptions {
|
||||
level: 'error';
|
||||
userId?: string;
|
||||
extra?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentry・OpenTelemetryなど、エラートラッキング/APMサービスごとの実装差異を隠蔽するための抽象。
|
||||
* 新しいサービスを追加する場合はこのインターフェースを実装するアダプタをこのディレクトリに追加し、
|
||||
* telemetry-registry.tsのinitTelemetry内で登録する。
|
||||
*/
|
||||
export interface TelemetryAdapter {
|
||||
captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void;
|
||||
startSpan<T>(name: string, fn: () => T): T;
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
39
packages/backend/src/core/telemetry/telemetry-registry.ts
Normal file
39
packages/backend/src/core/telemetry/telemetry-registry.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import { SentryTelemetryAdapter } from './adapters/SentryTelemetryAdapter.js';
|
||||
import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
|
||||
/**
|
||||
* NestのDIコンテナが構築される前(boot処理内)で初期化する必要があるため、
|
||||
* DIを介さないモジュールレベルの状態として有効なアダプタを保持する。
|
||||
* TelemetryServiceはこの状態への薄いラッパーとして振る舞う。
|
||||
*/
|
||||
const adapters: TelemetryAdapter[] = [];
|
||||
|
||||
export async function initTelemetry(config: Config): Promise<void> {
|
||||
if (config.sentryForBackend) {
|
||||
adapters.push(await SentryTelemetryAdapter.create(config.sentryForBackend));
|
||||
}
|
||||
}
|
||||
|
||||
export function captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
for (const adapter of adapters) {
|
||||
adapter.captureMessage(message, opts);
|
||||
}
|
||||
}
|
||||
|
||||
export function startSpan<T>(name: string, fn: () => T): T {
|
||||
const wrapped = adapters.reduceRight<() => T>(
|
||||
(inner, adapter) => () => adapter.startSpan(name, inner),
|
||||
fn,
|
||||
);
|
||||
return wrapped();
|
||||
}
|
||||
|
||||
export async function shutdownTelemetry(): Promise<void> {
|
||||
await Promise.all(adapters.map(adapter => adapter.shutdown()));
|
||||
}
|
||||
@@ -91,7 +91,5 @@ export const DI = {
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
noteDraftsRepository: Symbol('noteDraftsRepository'),
|
||||
worldRoomsRepository: Symbol('worldRoomsRepository'),
|
||||
worldAvatarsRepository: Symbol('worldAvatarsRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -75,8 +75,6 @@ import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-i
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
|
||||
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
|
||||
import { packedWorldRoomDetailedSchema, packedWorldRoomLiteSchema } from '@/models/json-schema/world-room.js';
|
||||
import { packedWorldAvatarDetailedSchema, packedWorldAvatarLiteSchema } from '@/models/json-schema/world-avatar.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -149,10 +147,6 @@ export const refs = {
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
WorldRoomLite: packedWorldRoomLiteSchema,
|
||||
WorldRoomDetailed: packedWorldRoomDetailedSchema,
|
||||
WorldAvatarLite: packedWorldAvatarLiteSchema,
|
||||
WorldAvatarDetailed: packedWorldAvatarDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
@@ -672,6 +672,11 @@ export class MiMeta {
|
||||
})
|
||||
public urlPreviewUserAgent: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 3072, array: true, default: '{}',
|
||||
})
|
||||
public urlPreviewSensitiveList: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
default: 'none',
|
||||
|
||||
@@ -84,8 +84,6 @@ import {
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -546,18 +544,6 @@ const $reversiGamesRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldRoomsRepository: Provider = {
|
||||
provide: DI.worldRoomsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldRoom).extend(miRepository as MiRepository<MiWorldRoom>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldAvatarsRepository: Provider = {
|
||||
provide: DI.worldAvatarsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldAvatar).extend(miRepository as MiRepository<MiWorldAvatar>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
@@ -637,8 +623,6 @@ const $worldAvatarsRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -717,8 +701,6 @@ const $worldAvatarsRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('world_avatar')
|
||||
export class MiWorldAvatar {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldAvatar>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export const worldRoomVisibility = ['public', 'private'] as const;
|
||||
export type WorldRoomVisibility = typeof worldRoomVisibility[number];
|
||||
|
||||
@Entity('world_room')
|
||||
export class MiWorldRoom {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public likedCount: number;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public accessCount: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, default: 'public',
|
||||
})
|
||||
public visibility: WorldRoomVisibility;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldRoom>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelMuting } from '@/models/ChannelMuting.js';
|
||||
import { MiChannelMuting } from "@/models/ChannelMuting.js";
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
@@ -84,8 +84,6 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
@@ -175,8 +173,6 @@ export {
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
@@ -257,5 +253,3 @@ export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & M
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type WorldRoomsRepository = Repository<MiWorldRoom> & MiRepository<MiWorldRoom>;
|
||||
export type WorldAvatarsRepository = Repository<MiWorldAvatar> & MiRepository<MiWorldAvatar>;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldAvatarLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldAvatarDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldRoomLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldRoomDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
attachedFiles: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -87,8 +87,6 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
@@ -256,8 +254,6 @@ export const entities = [
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
@@ -92,6 +93,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private config: Config,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private telemetryService: TelemetryService,
|
||||
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
|
||||
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
|
||||
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
|
||||
@@ -156,13 +158,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
}
|
||||
|
||||
let Sentry: typeof import('@sentry/node') | undefined;
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((mod) => {
|
||||
Sentry = mod;
|
||||
});
|
||||
}
|
||||
|
||||
//#region system
|
||||
{
|
||||
const processer = (job: Bull.Job) => {
|
||||
@@ -181,11 +176,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: System: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM),
|
||||
autorun: false,
|
||||
@@ -198,12 +189,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err: Error) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -238,11 +227,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: DB: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.DB),
|
||||
autorun: false,
|
||||
@@ -255,12 +240,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -270,11 +253,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region deliver
|
||||
{
|
||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.deliverProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: Deliver', () => this.deliverProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.DELIVER),
|
||||
autorun: false,
|
||||
@@ -295,12 +274,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -310,11 +287,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region inbox
|
||||
{
|
||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
|
||||
} else {
|
||||
return this.inboxProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: Inbox', () => this.inboxProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.INBOX),
|
||||
autorun: false,
|
||||
@@ -335,12 +308,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -350,11 +321,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region user-webhook deliver
|
||||
{
|
||||
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.userWebhookDeliverProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: UserWebhookDeliver', () => this.userWebhookDeliverProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
@@ -375,12 +342,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -390,11 +355,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region system-webhook deliver
|
||||
{
|
||||
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.systemWebhookDeliverProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: SystemWebhookDeliver', () => this.systemWebhookDeliverProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
@@ -415,12 +376,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -440,11 +399,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: Relationship: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP),
|
||||
autorun: false,
|
||||
@@ -462,12 +417,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -485,11 +438,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: ObjectStorage: ' + job.name, () => processer(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||
autorun: false,
|
||||
@@ -503,12 +452,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
}
|
||||
this.telemetryService.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
})
|
||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
||||
@@ -518,11 +465,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region ended poll notification
|
||||
{
|
||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
|
||||
} else {
|
||||
return this.endedPollNotificationProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: EndedPollNotification', () => this.endedPollNotificationProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||
autorun: false,
|
||||
@@ -533,11 +476,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region post scheduled note
|
||||
{
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
|
||||
} else {
|
||||
return this.postScheduledNoteProcessorService.process(job);
|
||||
}
|
||||
return this.telemetryService.startSpan('Queue: PostScheduledNote', () => this.postScheduledNoteProcessorService.process(job));
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE),
|
||||
autorun: false,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
@@ -31,6 +30,7 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { MainChannel } from './api/stream/channels/main.js';
|
||||
import { AdminChannel } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannel } from './api/stream/channels/antenna.js';
|
||||
@@ -49,7 +49,6 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from './api/stream/channels/world-room.js';
|
||||
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
|
||||
@@ -100,7 +99,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
QueueStatsChannel,
|
||||
ServerStatsChannel,
|
||||
UserListChannel,
|
||||
WorldRoomChannel,
|
||||
NoteStreamingHidingService,
|
||||
OpenApiServerService,
|
||||
OAuth2ProviderService,
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { MiMeta, UserIpsRepository } from '@/models/_.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
@@ -36,7 +37,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private userIpHistories: Map<MiUser['id'], Set<string>>;
|
||||
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
|
||||
private Sentry: typeof import('@sentry/node') | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
@@ -52,6 +52,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
private rateLimiterService: RateLimiterService,
|
||||
private roleService: RoleService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private telemetryService: TelemetryService,
|
||||
) {
|
||||
this.logger = this.apiLoggerService.logger;
|
||||
this.userIpHistories = new Map<MiUser['id'], Set<string>>();
|
||||
@@ -59,12 +60,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
this.userIpHistoriesClearIntervalId = setInterval(() => {
|
||||
this.userIpHistories.clear();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((Sentry) => {
|
||||
this.Sentry = Sentry;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#sendApiError(reply: FastifyReply, err: ApiError): void {
|
||||
@@ -126,24 +121,20 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
if (this.Sentry != null) {
|
||||
this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
user: {
|
||||
id: userId,
|
||||
this.telemetryService.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
userId,
|
||||
extra: {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
extra: {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: err.message,
|
||||
code: err.name,
|
||||
stack: err.stack,
|
||||
id: errId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
throw new ApiError(null, {
|
||||
e: {
|
||||
@@ -441,15 +432,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// API invoking
|
||||
if (this.Sentry != null) {
|
||||
return await this.Sentry.startSpan({
|
||||
name: 'API: ' + ep.name,
|
||||
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
} else {
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
||||
}
|
||||
return await this.telemetryService.startSpan('API: ' + ep.name, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -447,14 +447,4 @@ export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitati
|
||||
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
|
||||
export * as 'chat/history' from './endpoints/chat/history.js';
|
||||
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
|
||||
export * as 'world/rooms/create' from './endpoints/world/rooms/create.js';
|
||||
export * as 'world/rooms/update' from './endpoints/world/rooms/update.js';
|
||||
export * as 'world/rooms/delete' from './endpoints/world/rooms/delete.js';
|
||||
export * as 'world/rooms/list-by-user' from './endpoints/world/rooms/list-by-user.js';
|
||||
export * as 'world/rooms/show' from './endpoints/world/rooms/show.js';
|
||||
export * as 'world/avatars/create' from './endpoints/world/avatars/create.js';
|
||||
export * as 'world/avatars/update' from './endpoints/world/avatars/update.js';
|
||||
export * as 'world/avatars/delete' from './endpoints/world/avatars/delete.js';
|
||||
export * as 'world/avatars/list' from './endpoints/world/avatars/list.js';
|
||||
export * as 'world/avatars/show' from './endpoints/world/avatars/show.js';
|
||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||
|
||||
@@ -544,6 +544,14 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
urlPreviewSensitiveList: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
federation: {
|
||||
type: 'string',
|
||||
enum: ['all', 'specified', 'none'],
|
||||
@@ -760,6 +768,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||
urlPreviewSensitiveList: instance.urlPreviewSensitiveList,
|
||||
federation: instance.federation,
|
||||
federationHosts: instance.federationHosts,
|
||||
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
|
||||
|
||||
@@ -189,6 +189,12 @@ export const paramDef = {
|
||||
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||
urlPreviewSensitiveList: {
|
||||
type: 'array', nullable: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
}
|
||||
},
|
||||
federation: {
|
||||
type: 'string',
|
||||
enum: ['all', 'none', 'specified'],
|
||||
@@ -734,6 +740,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.urlPreviewSensitiveList)) {
|
||||
set.urlPreviewSensitiveList = ps.urlPreviewSensitiveList.filter(Boolean);
|
||||
}
|
||||
|
||||
if (ps.federation !== undefined) {
|
||||
set.federation = ps.federation;
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate avatar
|
||||
|
||||
const avatar = await this.worldAvatarService.create(me, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldAvatarEntityService.packDetailed(avatar);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
await this.worldAvatarService.delete(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||
|
||||
const avatars = await this.worldAvatarService.getMyAvatarsWithPagination(me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldAvatarEntityService.packDetailedMany(avatars, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findAvatarById(ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
if (avatar.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
return this.worldAvatarEntityService.packDetailed(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
// TODO: validate avatar
|
||||
|
||||
await this.worldAvatarService.update(avatar, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
active: ps.active,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
description: { type: 'string', maxLength: 1024 },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'visibility', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate room
|
||||
|
||||
const room = await this.worldRoomService.create(me, {
|
||||
name: ps.name,
|
||||
description: ps.description ?? '',
|
||||
visibility: ps.visibility,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldRoomEntityService.packDetailed(room);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
await this.worldRoomService.delete(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldRoom',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomLite',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
private worldRoomService: WorldRoomService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||
|
||||
const rooms = await this.worldRoomService.getRoomsOfUserWithPagination(ps.userId, ps.userId === me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldRoomEntityService.packLiteMany(rooms, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldRoom',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
if (room.userId !== me.id && room.visibility === 'private') {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
return this.worldRoomEntityService.packDetailed(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
description: { type: 'string', maxLength: 1024 },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
// TODO: validate room
|
||||
|
||||
await this.worldRoomService.update(room, {
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
visibility: ps.visibility,
|
||||
def: ps.def,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from '@/server/api/stream/channels/world-room.js';
|
||||
import type { ChannelRequest } from './channel.js';
|
||||
import type { ChannelConstructor } from './channel.js';
|
||||
import type Channel from './channel.js';
|
||||
@@ -339,7 +338,6 @@ export default class Connection {
|
||||
case 'chatRoom': return ChatRoomChannel;
|
||||
case 'reversi': return ReversiChannel;
|
||||
case 'reversiGame': return ReversiGameChannel;
|
||||
case 'worldRoom': return WorldRoomChannel;
|
||||
|
||||
default:
|
||||
throw new Error(`no such channel: ${name}`);
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomMultiplayService } from '@/core/WorldRoomMultiplayService.js';
|
||||
import Channel, { type ChannelRequest } from '../channel.js';
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class WorldRoomChannel extends Channel {
|
||||
public readonly chName = 'worldRoom';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = true as const;
|
||||
public static kind = 'read:worldRoom';
|
||||
private roomId: string;
|
||||
private intervalId: NodeJS.Timeout;
|
||||
private isEntered = false;
|
||||
|
||||
constructor(
|
||||
@Inject(REQUEST)
|
||||
request: ChannelRequest,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomMultiplayService: WorldRoomMultiplayService,
|
||||
) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: JsonObject): Promise<boolean> {
|
||||
if (typeof params.roomId !== 'string') return false;
|
||||
if (!this.user) return false;
|
||||
|
||||
this.roomId = params.roomId;
|
||||
|
||||
const room = await this.worldRoomService.findRoomById(this.roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
try {
|
||||
await this.enter();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.subscriber.on(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async enter() {
|
||||
if (this.isEntered) return;
|
||||
|
||||
await this.worldRoomMultiplayService.enter(this.user!.id, this.roomId);
|
||||
|
||||
this.isEntered = true;
|
||||
|
||||
this.send('entered', {
|
||||
playerProfiles: await this.worldRoomMultiplayService.getPlayerProfiles(this.roomId, this.user!.id),
|
||||
});
|
||||
|
||||
this.intervalId = setInterval(async () => {
|
||||
const states = await this.worldRoomMultiplayService.getPlayerStatesAndHeatbeat(this.user!.id, this.roomId);
|
||||
delete states[this.user!.id];
|
||||
this.send('sync', states);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onEvent(data: GlobalEvents['worldRoom']['payload']) {
|
||||
switch (data.type) {
|
||||
case 'enter': {
|
||||
if (data.body.user.id === this.user!.id) return; // 自分の入室は無視
|
||||
this.send('playerEntered', {
|
||||
id: data.body.user.id,
|
||||
profile: this.worldRoomMultiplayService.packPlayerProfile(data.body.user, data.body.avatar),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'left': {
|
||||
if (data.body.userId === this.user!.id) return; // 自分の退室は無視
|
||||
this.send('playerLeft', {
|
||||
id: data.body.userId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'update':
|
||||
if (this.roomId && this.isEntered) {
|
||||
this.worldRoomMultiplayService.updatePlayerState(this.user!.id, this.roomId, body);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
this.subscriber.off(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
clearInterval(this.intervalId);
|
||||
this.worldRoomMultiplayService.left(this.user!.id, this.roomId);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
@@ -29,6 +30,7 @@ export class UrlPreviewService {
|
||||
private meta: MiMeta,
|
||||
|
||||
private httpRequestService: HttpRequestService,
|
||||
private utilityService: UtilityService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('url-preview');
|
||||
@@ -95,6 +97,10 @@ export class UrlPreviewService {
|
||||
summary.icon = this.wrap(summary.icon);
|
||||
summary.thumbnail = this.wrap(summary.thumbnail);
|
||||
|
||||
if (summary.sensitive !== true) {
|
||||
summary.sensitive = this.utilityService.isKeyWordIncluded(summary.url, this.meta.urlPreviewSensitiveList);
|
||||
}
|
||||
|
||||
// Cache 1day
|
||||
reply.header('Cache-Control', 'max-age=86400, immutable');
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, beforeEach, test } from 'vitest';
|
||||
import { describe, beforeAll, beforeEach, test, vi } from 'vitest';
|
||||
import { UserToken, api, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('API visibility', () => {
|
||||
describe('Note visibility', () => {
|
||||
//#region vars
|
||||
@@ -409,10 +411,12 @@ describe('API visibility', () => {
|
||||
|
||||
//#region HTL
|
||||
test('[HTL] public-post が 自分が見れる', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === pub.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === pub.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('[HTL] public-post が 非フォロワーから見れない', async () => {
|
||||
@@ -423,10 +427,12 @@ describe('API visibility', () => {
|
||||
});
|
||||
|
||||
test('[HTL] followers-post が フォロワーから見れる', async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === fol.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/timeline', { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter(n => n.id === fol.id);
|
||||
assert.strictEqual(notes[0].text, 'x');
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, test } from 'vitest';
|
||||
import { describe, beforeAll, test, vi } from 'vitest';
|
||||
import { api, castAsError, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Block', () => {
|
||||
// alice blocks bob
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
@@ -75,13 +77,15 @@ describe('Block', () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
const res = await api('notes/local-timeline', {}, bob);
|
||||
const body = res.body as misskey.entities.Note[];
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, bob);
|
||||
const body = res.body as misskey.entities.Note[];
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
|
||||
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(body.some(note => note.id === aliceNote.id), false);
|
||||
assert.strictEqual(body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(body.some(note => note.id === carolNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { describe, beforeAll, test, expect } from 'vitest';
|
||||
import { describe, beforeAll, test, expect, vi } from 'vitest';
|
||||
// node-fetch only supports it's own Blob yet
|
||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||
import { Blob } from 'node-fetch';
|
||||
@@ -14,6 +14,8 @@ import { api, castAsError, initTestDb, post, role, signup, simpleGet, uploadFile
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Endpoints', () => {
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
@@ -1149,12 +1151,14 @@ describe('Endpoints', () => {
|
||||
visibility: 'followers',
|
||||
});
|
||||
|
||||
const res = await api('notes/timeline', {}, dave);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/timeline', {}, dave);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 1);
|
||||
assert.strictEqual(res.body[0].id, carolPost.id);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 1);
|
||||
assert.strictEqual(res.body[0].id, carolPost.id);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { beforeAll, describe, test } from 'vitest';
|
||||
import { beforeAll, describe, test, vi } from 'vitest';
|
||||
import { api, post, react, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Mute', () => {
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
@@ -67,13 +69,15 @@ describe('Mute', () => {
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
||||
@@ -83,13 +87,15 @@ describe('Mute', () => {
|
||||
renoteId: carolNote.id,
|
||||
});
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { beforeAll, describe, test } from 'vitest';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { beforeAll, describe, test, vi } from 'vitest';
|
||||
import { api, post, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('Renote Mute', () => {
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
@@ -36,16 +37,15 @@ describe('Renote Mute', () => {
|
||||
const carolRenote = await post(carol, { renoteId: bobNote.id });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
// redisに追加されるのを待つ
|
||||
await setTimeout(100);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
|
||||
@@ -53,16 +53,15 @@ describe('Renote Mute', () => {
|
||||
const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' });
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
|
||||
// redisに追加されるのを待つ
|
||||
await setTimeout(100);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
// #12956
|
||||
@@ -70,15 +69,14 @@ describe('Renote Mute', () => {
|
||||
const carolNote = await post(carol, { text: 'hi' });
|
||||
const bobRenote = await post(bob, { renoteId: carolNote.id });
|
||||
|
||||
// redisに追加されるのを待つ
|
||||
await setTimeout(100);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
const res = await api('notes/local-timeline', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
|
||||
assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
|
||||
test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
|
||||
|
||||
@@ -510,7 +510,7 @@ describe('Streaming', () => {
|
||||
test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => {
|
||||
const erinNote = await post(erin, { text: 'hi', visibility: 'followers' });
|
||||
const fired = await waitFire(
|
||||
erin, 'homeTimeline', // erin:home
|
||||
erin, 'hybridTimeline', // erin:Hybrid
|
||||
() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post
|
||||
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
|
||||
);
|
||||
@@ -521,7 +521,7 @@ describe('Streaming', () => {
|
||||
test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => {
|
||||
const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' });
|
||||
const fired = await waitFire(
|
||||
ayano, 'homeTimeline', // ayano:home
|
||||
ayano, 'hybridTimeline', // ayano:Hybrid
|
||||
() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post
|
||||
msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin
|
||||
);
|
||||
@@ -530,9 +530,12 @@ describe('Streaming', () => {
|
||||
});
|
||||
|
||||
test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => {
|
||||
// ayano は kyoko をフォローしているため kyoko の followers 投稿にリプライできるが、
|
||||
// erin は kyoko をフォローしていないため、そのリプライは erin の Hybrid Timeline には流れないはず
|
||||
const kyokoFollowersNote = await post(kyoko, { text: 'hi', visibility: 'followers' });
|
||||
const fired = await waitFire(
|
||||
erin, 'homeTimeline', // erin:home
|
||||
() => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post
|
||||
erin, 'hybridTimeline', // erin:Hybrid
|
||||
() => api('notes/create', { text: 'hello', replyId: kyokoFollowersNote.id }, ayano), // ayano reply to kyoko's followers post
|
||||
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
|
||||
);
|
||||
|
||||
@@ -680,7 +683,7 @@ describe('Streaming', () => {
|
||||
const fired = await waitFire(
|
||||
chitose, 'userList',
|
||||
() => api('notes/create', { text: 'foo' }, takumi),
|
||||
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||
msg => msg.type === 'note' && msg.body.userId === takumi.id,
|
||||
{ listId: list.id },
|
||||
);
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { beforeAll, describe, test } from 'vitest';
|
||||
import { beforeAll, describe, test, vi } from 'vitest';
|
||||
import { api, post, signup, uploadUrl } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
const waitForPushToTlOptions = { timeout: 3000, interval: 25 };
|
||||
|
||||
describe('users/notes', () => {
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let jpgNote: misskey.entities.Note;
|
||||
@@ -32,16 +34,18 @@ describe('users/notes', () => {
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
test('withFiles', async () => {
|
||||
const res = await api('users/notes', {
|
||||
userId: alice.id,
|
||||
withFiles: true,
|
||||
}, alice);
|
||||
await vi.waitFor(async () => {
|
||||
const res = await api('users/notes', {
|
||||
userId: alice.id,
|
||||
withFiles: true,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 3);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 3);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
|
||||
}, waitForPushToTlOptions);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,9 @@ import { beforeAll } from 'vitest';
|
||||
import { initTestDb, sendEnvResetRequest } from './utils.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await initTestDb(false);
|
||||
// 前ファイルのNestJSアプリをdispose(env-reset)した後にスキーマをdrop & 再作成する。
|
||||
// 逆順だと、前ファイルの最後のテストが投げっぱなしにした非同期処理(cacheServiceのrefresh等)が
|
||||
// dispose前のdrop中に発火し、Unhandled Rejection (relation does not exist) でクラッシュしうる。
|
||||
await sendEnvResetRequest();
|
||||
await initTestDb(false);
|
||||
});
|
||||
|
||||
60
packages/backend/test/unit/SentryTelemetryAdapter.ts
Normal file
60
packages/backend/test/unit/SentryTelemetryAdapter.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { buildSentryIntegrations } from '@/core/telemetry/adapters/SentryTelemetryAdapter.js';
|
||||
|
||||
type TestIntegration = Parameters<ReturnType<typeof buildSentryIntegrations>>[0][number];
|
||||
|
||||
function testIntegration(name: string): TestIntegration {
|
||||
return { name };
|
||||
}
|
||||
|
||||
describe('SentryTelemetryAdapter', () => {
|
||||
test('removes disabled integrations from Sentry defaults', () => {
|
||||
const integrations = buildSentryIntegrations({
|
||||
disabledIntegrations: ['Postgres'],
|
||||
enableNodeProfiling: false,
|
||||
});
|
||||
|
||||
const result = integrations([
|
||||
testIntegration('Http'),
|
||||
testIntegration('Postgres'),
|
||||
testIntegration('Redis'),
|
||||
]);
|
||||
|
||||
expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'Redis']);
|
||||
});
|
||||
|
||||
test('keeps profiling integration when enabled', () => {
|
||||
const integrations = buildSentryIntegrations({
|
||||
disabledIntegrations: [],
|
||||
enableNodeProfiling: true,
|
||||
nodeProfilingIntegration: () => testIntegration('ProfilingIntegration'),
|
||||
});
|
||||
|
||||
const result = integrations([
|
||||
testIntegration('Http'),
|
||||
]);
|
||||
|
||||
expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'ProfilingIntegration']);
|
||||
});
|
||||
|
||||
test('warns about unknown disabled integration names without removing defaults', () => {
|
||||
const warn = vi.fn();
|
||||
const integrations = buildSentryIntegrations({
|
||||
disabledIntegrations: ['Unknown'],
|
||||
enableNodeProfiling: false,
|
||||
warn,
|
||||
});
|
||||
|
||||
const result = integrations([
|
||||
testIntegration('Http'),
|
||||
]);
|
||||
|
||||
expect(result.map((integration: TestIntegration) => integration.name)).toEqual(['Http']);
|
||||
expect(warn).toHaveBeenCalledWith('Unknown Sentry integration configured in sentryForBackend.disabledIntegrations: Unknown');
|
||||
});
|
||||
});
|
||||
@@ -11,16 +11,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.9",
|
||||
"@types/node": "26.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@types/node": "26.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"rollup": "4.62.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"magic-string": "0.30.21",
|
||||
"oxc-walker": "1.0.0",
|
||||
"rolldown": "1.1.2",
|
||||
"rolldown": "1.1.3",
|
||||
"vite": "8.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"noEmit": true,
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
|
||||
@@ -8,5 +8,5 @@ export function assertNever(x: never): never {
|
||||
throw new Error(`Unexpected type: ${(x as any)?.type ?? x}`);
|
||||
}
|
||||
|
||||
export function assertType<T>(_node: unknown): asserts node is T {
|
||||
export function assertType<T>(_node: unknown): asserts _node is T {
|
||||
}
|
||||
|
||||
2
packages/frontend-embed/@types/theme.d.ts
vendored
2
packages/frontend-embed/@types/theme.d.ts
vendored
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
declare module '@@/themes/*.json5' {
|
||||
import { Theme } from '@/theme.js';
|
||||
import { Theme } from '@@/js/theme.js';
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/pluginutils": "5.4.0",
|
||||
"@vitejs/plugin-vue": "6.0.7",
|
||||
"buraha": "0.0.1",
|
||||
"frontend-shared": "workspace:*",
|
||||
"i18n": "workspace:*",
|
||||
@@ -21,42 +18,34 @@
|
||||
"mfm-js": "0.26.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.62.2",
|
||||
"shiki": "4.2.0",
|
||||
"shiki": "4.3.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"uuid": "14.0.0",
|
||||
"vue": "3.5.38"
|
||||
"uuid": "14.0.1",
|
||||
"vue": "3.5.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/pluginutils": "5.4.0",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.9",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/node": "26.0.0",
|
||||
"@types/node": "26.0.1",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"@vue/runtime-core": "3.5.38",
|
||||
"acorn": "8.17.0",
|
||||
"cross-env": "10.1.0",
|
||||
"@vitejs/plugin-vue": "6.0.7",
|
||||
"@vue/runtime-core": "3.5.39",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.9.2",
|
||||
"happy-dom": "20.10.6",
|
||||
"intersection-observer": "0.12.2",
|
||||
"lightningcss": "1.32.0",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.14.6",
|
||||
"prettier": "3.8.4",
|
||||
"sass-embedded": "1.100.0",
|
||||
"start-server-and-test": "3.0.11",
|
||||
"tsx": "4.22.4",
|
||||
"vite": "8.1.0",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.3.5",
|
||||
"vue-eslint-parser": "10.4.1",
|
||||
"vue-tsc": "3.3.5"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"removeComments": false,
|
||||
"skipLibCheck": true,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import locales from 'i18n';
|
||||
@@ -11,7 +11,7 @@ import pluginJson5 from './lib/vite-plugin-json5.js';
|
||||
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n';
|
||||
import { Features } from 'lightningcss';
|
||||
|
||||
const url = process.env.NODE_ENV === 'development' ? (yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
|
||||
const url = process.env.NODE_ENV === 'development' ? (loadYaml(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
|
||||
const host = url ? (new URL(url)).hostname : undefined;
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# frontend用Misskey Worldエンジン
|
||||
|
||||
エンジンはWeb Worker内で動作し、ほぼすべてのMisskey Webの機能は使えないため、意図しないそれらへの参照/依存が原理的に発生しないように別パッケージとする
|
||||
|
||||
ただしヘッドレス動作することは(今のところ)意図していない
|
||||
@@ -1,28 +0,0 @@
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import sharedConfig from '../shared/eslint.config.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules',
|
||||
'built',
|
||||
'coverage',
|
||||
'jest.config.ts',
|
||||
'test',
|
||||
'test-d',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "frontend-misskey-world-engine",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.2",
|
||||
"@typescript-eslint/parser": "8.59.2",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.14",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "9.15.0",
|
||||
"@babylonjs/inspector": "9.15.0",
|
||||
"@babylonjs/loaders": "9.15.0",
|
||||
"@babylonjs/materials": "9.15.0",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"eventemitter3": "5.0.4",
|
||||
"seedrandom": "3.0.5",
|
||||
"tinycolor2": "1.6.0",
|
||||
"hls.js": "1.6.16"
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
const IN_WEB_WORKER = typeof window === 'undefined';
|
||||
|
||||
export type EngineBaseEvents = {
|
||||
'loadingProgress': (ctx: { progress: number }) => void;
|
||||
'contextlost': (ctx: { reason: string; message: string; }) => void;
|
||||
};
|
||||
|
||||
export abstract class EngineBase<EVs extends EngineBaseEvents> extends EventEmitter<{
|
||||
'ev': (ctx: { type: keyof EVs; ctx: Parameters<EVs[keyof EVs]>[0] }) => void;
|
||||
}> {
|
||||
declare _eventTypes?: EVs;
|
||||
|
||||
protected engine: BABYLON.WebGPUEngine;
|
||||
public scene: BABYLON.Scene;
|
||||
protected fps: number | null = null;
|
||||
protected disposed = false;
|
||||
|
||||
public inputs: EventEmitter<{
|
||||
'click': (event: { x: number; y: number; }) => void;
|
||||
'keydown': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
'keyup': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
'wheel': (event: { deltaY: number; }) => void;
|
||||
'zoom': (event: { delta: number; }) => void;
|
||||
'pointer': (event: { x: number; y: number; }) => void;
|
||||
}> = new EventEmitter();
|
||||
|
||||
constructor(options: {
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
fps: number | null;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.fps = options.fps;
|
||||
|
||||
this.engine = options.engine;
|
||||
// doNotHandleContextLostがtrueだとそもそも呼ばれない
|
||||
//babylonEngine.onContextLostObservable.add(() => {
|
||||
// os.alert({
|
||||
// type: 'error',
|
||||
// title: i18n.ts.somethingHappened,
|
||||
// text: i18n.ts._miWorld.crushed_description,
|
||||
// });
|
||||
//});
|
||||
this.engine._device.lost.then((info) => { // TODO: babylonEngineの内部プロパティに依存しない方法をforumで聞く
|
||||
this.ev('contextlost', { reason: info.reason, message: info.message }); // transferableじゃないデータが含まれている可能性も考慮してinfoそのままは送らない
|
||||
});
|
||||
|
||||
this.scene = new BABYLON.Scene(this.engine);
|
||||
}
|
||||
|
||||
private currentRafId: number | null = null;
|
||||
|
||||
protected startRenderLoop() {
|
||||
if (this.fps == null) {
|
||||
this.engine.runRenderLoop(() => {
|
||||
this.scene.render();
|
||||
});
|
||||
} else {
|
||||
let then = 0;
|
||||
const interval = 1000 / this.fps;
|
||||
|
||||
const renderLoop = (timeStamp: number) => {
|
||||
if (this.disposed) return;
|
||||
|
||||
// workerで実行される可能性がある
|
||||
this.currentRafId = requestAnimationFrame(renderLoop);
|
||||
|
||||
const delta = timeStamp - then;
|
||||
if (delta <= interval) return;
|
||||
then = timeStamp - (delta % interval);
|
||||
|
||||
this.engine.beginFrame();
|
||||
this.scene.render();
|
||||
this.engine.endFrame();
|
||||
};
|
||||
|
||||
// workerで実行される可能性がある
|
||||
this.currentRafId = requestAnimationFrame(renderLoop);
|
||||
}
|
||||
}
|
||||
|
||||
public pauseRender() { // TODO: srと同じく参照カウント方式にした方が便利そう
|
||||
this.engine.stopRenderLoop();
|
||||
if (this.currentRafId != null) {
|
||||
// workerで実行される可能性がある
|
||||
cancelAnimationFrame(this.currentRafId);
|
||||
this.currentRafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public resumeRender() {
|
||||
this.startRenderLoop();
|
||||
}
|
||||
|
||||
public abstract init(): Promise<void>;
|
||||
|
||||
protected ev<K extends keyof EVs>(type: K, ctx: Parameters<EVs[K]>[0]) {
|
||||
this.emit('ev', { type, ctx });
|
||||
}
|
||||
|
||||
public async takeScreenshot() {
|
||||
return await BABYLON.Tools.CreateScreenshotAsync(this.engine, this.scene.activeCamera!, { precision: 1 });
|
||||
}
|
||||
|
||||
public abstract resize(): void;
|
||||
|
||||
public destroy() {
|
||||
this.engine.stopRenderLoop();
|
||||
if (this.currentRafId != null) {
|
||||
// workerで実行される可能性がある
|
||||
cancelAnimationFrame(this.currentRafId);
|
||||
this.currentRafId = null;
|
||||
}
|
||||
this.engine.dispose();
|
||||
this.scene.dispose();
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { AccessoryContainer } from './avatars/AccessoryContainer.js';
|
||||
import { getAccessoryDef } from './avatars/accessory-defs.js';
|
||||
import { createTextMesh, Timer } from './utility.js';
|
||||
import type { WorldAvatar } from 'misskey-world/src/types.js';
|
||||
|
||||
export type PlayerProfile = {
|
||||
user: {
|
||||
name: string;
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
} | null;
|
||||
avatar: WorldAvatar;
|
||||
};
|
||||
|
||||
export type PlayerState = {
|
||||
position: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
sit?: string; // id
|
||||
};
|
||||
|
||||
const DEFAULT_FACE_PARTS_EYES = {
|
||||
'_none_': null,
|
||||
'a': '/client-assets/world/avatars/eyes-a.png',
|
||||
'b': '/client-assets/world/avatars/eyes-b.png',
|
||||
'c': '/client-assets/world/avatars/eyes-c.png',
|
||||
'd': '/client-assets/world/avatars/eyes-d.png',
|
||||
'e': '/client-assets/world/avatars/eyes-e.png',
|
||||
'f': '/client-assets/world/avatars/eyes-f.png',
|
||||
'g': '/client-assets/world/avatars/eyes-g.png',
|
||||
};
|
||||
|
||||
const DEFAULT_FACE_PARTS_MOUTH = {
|
||||
'_none_': null,
|
||||
'a': '/client-assets/world/avatars/mouth-a.png',
|
||||
'b': '/client-assets/world/avatars/mouth-b.png',
|
||||
'c': '/client-assets/world/avatars/mouth-c.png',
|
||||
'd': '/client-assets/world/avatars/mouth-d.png',
|
||||
'e': '/client-assets/world/avatars/mouth-e.png',
|
||||
'f': '/client-assets/world/avatars/mouth-f.png',
|
||||
'g': '/client-assets/world/avatars/mouth-g.png',
|
||||
'h': '/client-assets/world/avatars/mouth-h.png',
|
||||
'i': '/client-assets/world/avatars/mouth-i.png',
|
||||
};
|
||||
|
||||
let usernameLabelMaterial: BABYLON.StandardMaterial | null = null;
|
||||
|
||||
export class PlayerContainer {
|
||||
public id: string;
|
||||
private profile: PlayerProfile;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRootContainerForAnim: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode;
|
||||
private modelRoot: BABYLON.TransformNode | null = null;
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private animationObserver: BABYLON.Observer<BABYLON.Scene> | null = null;
|
||||
private accessoryContainers: AccessoryContainer[] = [];
|
||||
private timer: Timer = new Timer();
|
||||
private showUsername: boolean;
|
||||
private show2dAvatar: boolean;
|
||||
private usernameLabelMesh: BABYLON.Mesh | null = null;
|
||||
private twodAvatarMesh: BABYLON.Mesh | null = null;
|
||||
|
||||
constructor(params: { id: string; profile: PlayerProfile; state: PlayerState | null; sr: BABYLON.SnapshotRenderingHelper; scene: BABYLON.Scene; showUsername: boolean; show2dAvatar: boolean; }) {
|
||||
this.id = params.id;
|
||||
this.profile = params.profile;
|
||||
this.sr = params.sr;
|
||||
this.scene = params.scene;
|
||||
|
||||
this.root = new BABYLON.TransformNode(`player:${this.id}`, params.scene);
|
||||
this.root.rotationQuaternion = null;
|
||||
this.subRootContainerForAnim = new BABYLON.TransformNode(`player:${this.id}:subRootContainerForAnim`, params.scene);
|
||||
this.subRootContainerForAnim.parent = this.root;
|
||||
this.subRoot = new BABYLON.TransformNode(`player:${this.id}:subRoot`, params.scene);
|
||||
this.subRoot.parent = this.subRootContainerForAnim;
|
||||
|
||||
this.showUsername = params.showUsername;
|
||||
this.show2dAvatar = params.show2dAvatar;
|
||||
this.applyInfoMesh();
|
||||
|
||||
if (params.state) this.applyState(params.state, true);
|
||||
}
|
||||
|
||||
public async loadAvatar() {
|
||||
const filePath = '/client-assets/world/avatars/default.glb';
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const modelRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.modelRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.modelRoot.parent = this.subRoot;
|
||||
this.modelRoot.scaling.x = -1;
|
||||
this.modelRoot.scaling = this.modelRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of modelRootMesh.getChildren()) {
|
||||
if (m.parent === modelRootMesh) {
|
||||
m.parent = this.modelRoot;
|
||||
}
|
||||
}
|
||||
|
||||
modelRootMesh.dispose();
|
||||
|
||||
const eyesBlinkTexture = new BABYLON.Texture('/client-assets/world/avatars/eyes-blink.png', this.scene, false, false);
|
||||
eyesBlinkTexture.hasAlpha = true;
|
||||
|
||||
let eyesTex: BABYLON.Texture | null = null;
|
||||
if (this.profile.avatar.eyes.type in DEFAULT_FACE_PARTS_EYES) {
|
||||
const eyesTexPath = DEFAULT_FACE_PARTS_EYES[this.profile.avatar.eyes.type];
|
||||
if (eyesTexPath) {
|
||||
eyesTex = new BABYLON.Texture(eyesTexPath, this.scene, false, false);
|
||||
eyesTex.hasAlpha = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mouthTex: BABYLON.Texture | null = null;
|
||||
if (this.profile.avatar.mouth.type in DEFAULT_FACE_PARTS_MOUTH) {
|
||||
const mouthTexPath = DEFAULT_FACE_PARTS_MOUTH[this.profile.avatar.mouth.type];
|
||||
if (mouthTexPath) {
|
||||
mouthTex = new BABYLON.Texture(mouthTexPath, this.scene, false, false);
|
||||
mouthTex.hasAlpha = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const mesh of this.modelRoot.getChildMeshes()) {
|
||||
if (mesh.name.includes('__BODY__')) {
|
||||
mesh.material.albedoColor = new BABYLON.Color3(this.profile.avatar.body.color[0], this.profile.avatar.body.color[1], this.profile.avatar.body.color[2]);
|
||||
}
|
||||
if (mesh.name.includes('__EYES__')) {
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(this.profile.avatar.eyes.color[0], this.profile.avatar.eyes.color[1], this.profile.avatar.eyes.color[2]);
|
||||
mat.albedoTexture = eyesTex;
|
||||
mat.roughness = 1;
|
||||
mat.metallic = 0;
|
||||
mesh.material = mat;
|
||||
|
||||
// TODO: SRを無効にせずに表現する方法を考える
|
||||
const blink = () => {
|
||||
if (mesh.isDisposed()) return;
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
mat.albedoTexture = eyesBlinkTexture;
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
this.sr.disableSnapshotRendering();
|
||||
mat.albedoTexture = eyesTex;
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
blink();
|
||||
}, Math.random() * 10000);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
blink();
|
||||
}, Math.random() * 10000);
|
||||
}
|
||||
if (mesh.name.includes('__MOUTH__')) {
|
||||
if (mouthTex != null) {
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(this.profile.avatar.mouth.color[0], this.profile.avatar.mouth.color[1], this.profile.avatar.mouth.color[2]);
|
||||
mat.albedoTexture = mouthTex;
|
||||
mat.roughness = 1;
|
||||
mat.metallic = 0;
|
||||
mesh.material = mat;
|
||||
} else {
|
||||
mesh.isVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.registerMeshes(this.modelRoot.getChildMeshes());
|
||||
|
||||
this.accessoryContainers = await Promise.all(this.profile.avatar.accessories.map(ac => this.loadAccessory({
|
||||
type: ac.type,
|
||||
id: ac.id,
|
||||
position: new BABYLON.Vector3(0, cm(19), 0),
|
||||
rotation: new BABYLON.Vector3(0, 0, 0),
|
||||
options: ac.options,
|
||||
})));
|
||||
|
||||
const anim = new BABYLON.Animation('', 'position.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: cm(0) },
|
||||
{ frame: 30, value: cm(-2) },
|
||||
{ frame: 60, value: cm(0) },
|
||||
{ frame: 90, value: cm(2) },
|
||||
{ frame: 120, value: cm(0) },
|
||||
]);
|
||||
this.subRootContainerForAnim.animations = [anim];
|
||||
this.animationObserver = this.scene.onAfterAnimationsObservable.add(() => {
|
||||
this.sr.updateMesh(this.subRootContainerForAnim.getChildMeshes(), false);
|
||||
});
|
||||
this.scene.beginAnimation(this.subRootContainerForAnim, 0, 120, true);
|
||||
}
|
||||
|
||||
private async loadAccessory(args: {
|
||||
type: string;
|
||||
id: string;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
options: Record<string, unknown>;
|
||||
}) {
|
||||
const def = getAccessoryDef(args.type);
|
||||
|
||||
const container = new AccessoryContainer({
|
||||
id: args.id,
|
||||
type: args.type,
|
||||
position: args.position.clone(),
|
||||
rotation: args.rotation.clone(),
|
||||
options: args.options,
|
||||
sr: this.sr,
|
||||
getIsSrReady: () => true,
|
||||
lightContainer: this.lightContainer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
scene: this.scene,
|
||||
});
|
||||
container.registerMeshes = (meshes) => {
|
||||
this.registerMeshes(meshes);
|
||||
};
|
||||
|
||||
await container.load();
|
||||
|
||||
container.root.parent = this.subRoot;
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private applyInfoMesh() {
|
||||
if (this.showUsername ) {
|
||||
if (this.usernameLabelMesh == null) {
|
||||
if (usernameLabelMaterial == null) {
|
||||
const usernameLabelTex = new BABYLON.Texture('/client-assets/world/chars-black.png', this.scene, false, false);
|
||||
usernameLabelMaterial = new BABYLON.StandardMaterial('usernameLabelMaterial', this.scene);
|
||||
usernameLabelMaterial.roughness = 1;
|
||||
usernameLabelMaterial.diffuseColor = new BABYLON.Color3(1, 1, 1);
|
||||
usernameLabelMaterial.diffuseTexture = usernameLabelTex;
|
||||
usernameLabelMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
usernameLabelMaterial.emissiveTexture = usernameLabelTex;
|
||||
usernameLabelMaterial.disableLighting = true;
|
||||
}
|
||||
|
||||
this.usernameLabelMesh = createTextMesh(this.profile.user?.username ?? '(anonymous)', {
|
||||
size: cm(5),
|
||||
material: usernameLabelMaterial,
|
||||
});
|
||||
this.usernameLabelMesh.parent = this.subRoot;
|
||||
this.usernameLabelMesh.position.y = cm(40);
|
||||
this.usernameLabelMesh.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
|
||||
this.scene.addMesh(this.usernameLabelMesh);
|
||||
}
|
||||
} else {
|
||||
if (this.usernameLabelMesh != null) {
|
||||
this.usernameLabelMesh.dispose();
|
||||
this.scene.removeMesh(this.usernameLabelMesh);
|
||||
this.usernameLabelMesh = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.show2dAvatar && this.profile.user?.avatarUrl != null) {
|
||||
if (this.twodAvatarMesh == null) {
|
||||
const twodAvatarTex = new BABYLON.Texture(this.profile.user.avatarUrl, this.scene, false, true);
|
||||
const twodAvatarMat = new BABYLON.StandardMaterial('twodAvatarMat', this.scene);
|
||||
twodAvatarMat.roughness = 1;
|
||||
twodAvatarMat.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
twodAvatarMat.diffuseTexture = twodAvatarTex;
|
||||
twodAvatarMat.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
twodAvatarMat.emissiveTexture = twodAvatarTex;
|
||||
twodAvatarMat.disableLighting = true;
|
||||
twodAvatarMat.backFaceCulling = false;
|
||||
|
||||
this.twodAvatarMesh = BABYLON.MeshBuilder.CreatePlane('twodAvatar', { size: cm(10) }, this.scene);
|
||||
this.twodAvatarMesh.material = twodAvatarMat;
|
||||
this.twodAvatarMesh.parent = this.subRoot;
|
||||
this.twodAvatarMesh.position.y = cm(40) + cm(7.5);
|
||||
this.twodAvatarMesh.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
|
||||
this.scene.addMesh(this.twodAvatarMesh);
|
||||
}
|
||||
} else {
|
||||
if (this.twodAvatarMesh != null) {
|
||||
this.twodAvatarMesh.dispose(false, true);
|
||||
this.scene.removeMesh(this.twodAvatarMesh);
|
||||
this.twodAvatarMesh = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateUserInfoDisplayOptions(options: { showUsername: boolean; show2dAvatar: boolean; }) {
|
||||
this.showUsername = options.showUsername;
|
||||
this.show2dAvatar = options.show2dAvatar;
|
||||
this.applyInfoMesh();
|
||||
}
|
||||
|
||||
public applyState(state: PlayerState, forInit = false) {
|
||||
this.root.position.set(...state.position);
|
||||
this.subRoot.rotation.set(...state.rotation);
|
||||
if (!forInit) {
|
||||
const meshes = this.root.getChildMeshes();
|
||||
if (meshes.length > 0) this.sr.updateMesh(meshes);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.timer.dispose();
|
||||
if (this.animationObserver != null) {
|
||||
this.scene.onAfterAnimationsObservable.remove(this.animationObserver);
|
||||
}
|
||||
for (const ac of this.accessoryContainers) {
|
||||
ac.destroy();
|
||||
}
|
||||
this.accessoryContainers = [];
|
||||
if (this.usernameLabelMesh != null) {
|
||||
this.usernameLabelMesh.dispose();
|
||||
this.scene.removeMesh(this.usernameLabelMesh);
|
||||
this.usernameLabelMesh = null;
|
||||
}
|
||||
if (this.twodAvatarMesh != null) {
|
||||
this.twodAvatarMesh.dispose(false, true);
|
||||
this.scene.removeMesh(this.twodAvatarMesh);
|
||||
this.twodAvatarMesh = null;
|
||||
}
|
||||
this.root.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { ArcRotateCameraManualInput, getMeshesBoundingBox, GRAPHICS_QUALITY } from './utility.js';
|
||||
import { PlayerContainer, type PlayerProfile } from './PlayerContainer.js';
|
||||
import { EngineBase } from './EngineBase.js';
|
||||
import { deepClone } from './clone.js';
|
||||
import type { WorldAvatar } from 'misskey-world/src/types.js';
|
||||
|
||||
export class AvatarPreviewEngine extends EngineBase<{ // PlayerPreviewEngineに改名した方がいいかもしれない
|
||||
'loadingProgress': (ctx: { progress: number }) => void;
|
||||
'contextlost': (ctx: { reason: string; message: string; }) => void;
|
||||
}> {
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private shadowGenerator: BABYLON.ShadowGenerator;
|
||||
private camera: BABYLON.ArcRotateCamera;
|
||||
private avatarOptions: WorldAvatar | null = null;
|
||||
private playerContainer: PlayerContainer | null = null;
|
||||
private envMapIndoor: BABYLON.CubeTexture;
|
||||
private roomLight: BABYLON.SpotLight;
|
||||
private pipeline: BABYLON.DefaultRenderingPipeline;
|
||||
private graphicsQuality: number;
|
||||
private profile: PlayerProfile;
|
||||
|
||||
constructor(profile: PlayerProfile, options: {
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
graphicsQuality: number;
|
||||
fps: number | null;
|
||||
}) {
|
||||
super({
|
||||
engine: options.engine,
|
||||
fps: options.fps,
|
||||
});
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.graphicsQuality = options.graphicsQuality;
|
||||
this.profile = deepClone(profile);
|
||||
|
||||
this.scene.autoClear = false;
|
||||
this.scene.skipPointerMovePicking = true;
|
||||
this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため
|
||||
this.scene.clearColor = new BABYLON.Color4(0.01, 0.01, 0.01, 1);
|
||||
|
||||
this.sr = new BABYLON.SnapshotRenderingHelper(this.scene);
|
||||
|
||||
this.camera = new BABYLON.ArcRotateCamera('camera', Math.PI / 2, Math.PI / 2.5, cm(300), new BABYLON.Vector3(0, cm(90), 0), this.scene);
|
||||
this.camera.minZ = cm(1);
|
||||
this.camera.maxZ = cm(100000);
|
||||
this.camera.fov = 0.5;
|
||||
this.camera.lowerRadiusLimit = cm(50);
|
||||
this.camera.upperRadiusLimit = cm(1000);
|
||||
this.camera.inputs.clear();
|
||||
this.camera.inputs.add(new ArcRotateCameraManualInput(this.scene, {
|
||||
rotationSensitivity: 0.0005,
|
||||
}));
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
this.envMapIndoor.level = 0.6;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(cm(50), cm(249), cm(50)), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(500);
|
||||
this.roomLight.radius = cm(30);
|
||||
this.roomLight.intensity = 15 * WORLD_SCALE * WORLD_SCALE;
|
||||
|
||||
this.shadowGenerator = new BABYLON.ShadowGenerator(2048, this.roomLight);
|
||||
this.shadowGenerator.forceBackFacesOnly = true;
|
||||
this.shadowGenerator.bias = 0.0001;
|
||||
this.shadowGenerator.usePercentageCloserFiltering = true;
|
||||
this.shadowGenerator.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
this.shadowGenerator.getShadowMap().refreshRate = 60;
|
||||
|
||||
const gl = new BABYLON.GlowLayer('glow', this.scene, {
|
||||
blurKernelSize: 64,
|
||||
});
|
||||
gl.intensity = 0.5;
|
||||
this.scene.setRenderingAutoClearDepthStencil(gl.renderingGroupId, false);
|
||||
this.sr.updateMeshesForEffectLayer(gl);
|
||||
|
||||
this.pipeline = new BABYLON.DefaultRenderingPipeline('default', true, this.scene);
|
||||
this.pipeline.samples = 4;
|
||||
if (this.graphicsQuality >= GRAPHICS_QUALITY.HIGH) {
|
||||
this.pipeline.bloomEnabled = true;
|
||||
this.pipeline.bloomThreshold = 0.95;
|
||||
this.pipeline.bloomWeight = 0.1;
|
||||
this.pipeline.bloomKernel = 256;
|
||||
this.pipeline.bloomScale = 2;
|
||||
}
|
||||
this.pipeline.sharpenEnabled = true;
|
||||
this.pipeline.sharpen.edgeAmount = 0.5;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.startRenderLoop();
|
||||
|
||||
await this.scene.whenReadyAsync();
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.inputs.on('wheel', (ev) => {
|
||||
this.camera.fov += ev.deltaY * 0.0005;
|
||||
this.camera.fov = Math.max(0.25, Math.min(0.5, this.camera.fov));
|
||||
});
|
||||
|
||||
this.inputs.on('zoom', (ev) => {
|
||||
this.camera.fov += -ev.delta * 0.0015;
|
||||
this.camera.fov = Math.max(0.25, Math.min(0.5, this.camera.fov));
|
||||
});
|
||||
|
||||
this.inputs.on('pointer', (ev) => {
|
||||
(this.camera.inputs.attached.manual as ArcRotateCameraManualInput).setRotationVector({ x: ev.x, y: ev.y });
|
||||
});
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
private async load() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
|
||||
this.playerContainer = new PlayerContainer({
|
||||
id: '',
|
||||
profile: this.profile,
|
||||
state: {
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
},
|
||||
sr: this.sr,
|
||||
scene: this.scene,
|
||||
showUsername: false,
|
||||
show2dAvatar: false,
|
||||
});
|
||||
this.playerContainer.registerMeshes = (meshes) => {
|
||||
for (const mesh of meshes) {
|
||||
mesh.receiveShadows = true;
|
||||
this.shadowGenerator.addShadowCaster(mesh);
|
||||
|
||||
if (mesh.material) {
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
(subMat as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(subMat as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
(subMat as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため
|
||||
}
|
||||
} else {
|
||||
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
(mesh.material as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scene.meshes.includes(mesh)) this.scene.addMesh(mesh);
|
||||
}
|
||||
};
|
||||
|
||||
await this.playerContainer.loadAvatar();
|
||||
|
||||
const boundingInfo = getMeshesBoundingBox(this.playerContainer.root.getChildMeshes().filter(m => m.isEnabled() && m.isVisible), true);
|
||||
|
||||
this.camera.setTarget(new BABYLON.Vector3(0, boundingInfo.centerWorld.y, 0));
|
||||
|
||||
// zoom to fit
|
||||
const size = boundingInfo.extendSize;
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
this.camera.radius = distance * 5;
|
||||
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public clearPlayer() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
if (this.playerContainer != null) {
|
||||
this.playerContainer.destroy();
|
||||
this.playerContainer = null;
|
||||
}
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public async updateAvatar(value: WorldAvatar) {
|
||||
this.profile.avatar = value;
|
||||
this.clearPlayer();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
public resize() {
|
||||
// 一旦snapshot renderingを無効にしておかないとエラーが出る(babylonのバグ?)
|
||||
// ~~...が、一旦無効にしたらしたで複数のマテリアルがそれぞれ入れ替わる(?)という謎の現象が発生するためコメントアウトしとく(エラー出てもレンダリングが止まったりするわけでもないし)~~
|
||||
// ↑追記: engine.resizeした後に一瞬待つことで回避できることが判明
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.engine.resize(true);
|
||||
// workerで実行される可能性がある
|
||||
|
||||
setTimeout(() => {
|
||||
this.sr.enableSnapshotRendering();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.playerContainer?.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { AvatarPreviewEngine } from './avatarPreviewEngine.js';
|
||||
import { registerBabylonRuntime } from './babylonRuntime.js';
|
||||
import type { PlayerProfile } from './PlayerContainer.js';
|
||||
|
||||
registerBabylonRuntime();
|
||||
|
||||
export async function createAvatarPreviewEngine(params: {
|
||||
canvas: HTMLCanvasElement; options: { graphicsQuality: number; resolution: number; fps: number | null }; profile: PlayerProfile;
|
||||
}) {
|
||||
const babylonEngine = new BABYLON.WebGPUEngine(params.canvas, { doNotHandleContextLost: true, powerPreference: 'low-power', antialias: true });
|
||||
babylonEngine.compatibilityMode = false;
|
||||
babylonEngine.enableOfflineSupport = false;
|
||||
await babylonEngine.initAsync();
|
||||
if (params.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5);
|
||||
if (params.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2);
|
||||
|
||||
const engine = new AvatarPreviewEngine(params.profile, {
|
||||
engine: babylonEngine,
|
||||
...params.options,
|
||||
});
|
||||
|
||||
return engine;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { AvatarPreviewEngine } from './avatarPreviewEngine.js';
|
||||
import { registerBabylonRuntime } from './babylonRuntime.js';
|
||||
import type { PlayerProfile } from './PlayerContainer.js';
|
||||
|
||||
registerBabylonRuntime();
|
||||
|
||||
let engine: AvatarPreviewEngine | null = null;
|
||||
let canvas: OffscreenCanvas | null = null;
|
||||
|
||||
// TODO: 他のWorkerと実装を共通化
|
||||
onmessage = async (event) => {
|
||||
//console.log('Worker received message:', event.data);
|
||||
|
||||
switch (event.data?.type) {
|
||||
case 'init': {
|
||||
const profile = event.data.profile as PlayerProfile;
|
||||
canvas = event.data.canvas as OffscreenCanvas;
|
||||
const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true, powerPreference: 'low-power', antialias: true });
|
||||
babylonEngine.compatibilityMode = false;
|
||||
babylonEngine.enableOfflineSupport = false;
|
||||
await babylonEngine.initAsync();
|
||||
if (event.data.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5);
|
||||
if (event.data.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2);
|
||||
|
||||
engine = new AvatarPreviewEngine(profile, {
|
||||
engine: babylonEngine,
|
||||
...event.data.options,
|
||||
});
|
||||
|
||||
engine.on('ev', ({ type, ctx }) => {
|
||||
self.postMessage({ type: 'ev', ev: { type, ctx } });
|
||||
});
|
||||
|
||||
await engine.init();
|
||||
|
||||
self.postMessage({ type: 'inited' });
|
||||
break;
|
||||
}
|
||||
case 'resize': {
|
||||
canvas.width = event.data.width;
|
||||
canvas.height = event.data.height;
|
||||
if (engine != null) engine.resize();
|
||||
break;
|
||||
}
|
||||
case 'input:keydown': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('keydown', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:keyup': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('keyup', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:click': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('click', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:wheel': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('wheel', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:zoom': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('zoom', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:pointer': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('pointer', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'call': {
|
||||
if (engine == null) {
|
||||
console.error('Failed to call: Engine is not initialized yet!!!');
|
||||
break;
|
||||
}
|
||||
const res = engine[event.data.fn](...(event.data.args ?? []));
|
||||
if (event.data.needReturnValue) {
|
||||
if (res instanceof Promise) {
|
||||
res.then((r) => {
|
||||
self.postMessage({ type: 'return', id: event.data.id, value: r });
|
||||
});
|
||||
} else {
|
||||
self.postMessage({ type: 'return', id: event.data.id, value: res });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'set': {
|
||||
if (engine == null) {
|
||||
console.error('Failed to set: Engine is not initialized yet!!!');
|
||||
break;
|
||||
}
|
||||
engine[event.data.key] = event.data.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Unrecognized message type:', event.data?.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { camelToKebab, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { ModelExplorer, scaleMorph, Timer } from '../utility.js';
|
||||
import { convertRawOptions, type ConvertedOptions, type RawOptions } from '../mono.js';
|
||||
import { getAccessoryDef } from './accessory-defs.js';
|
||||
import type { AvatarAccessoryInstance } from './accessory.js';
|
||||
|
||||
export class AccessoryContainer {
|
||||
public id: string;
|
||||
public type: string;
|
||||
private options: ConvertedOptions;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode | null = null;
|
||||
public instance: AvatarAccessoryInstance | null = null;
|
||||
public model: ModelExplorer | null = null;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private getIsSrReady: () => boolean;
|
||||
private lightContainer: BABYLON.ClusteredLightContainer;
|
||||
private graphicsQuality: number;
|
||||
private timer: Timer = new Timer();
|
||||
|
||||
constructor(args: {
|
||||
id: string;
|
||||
type: string;
|
||||
options: RawOptions;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
sr: BABYLON.SnapshotRenderingHelper;
|
||||
getIsSrReady: () => boolean;
|
||||
lightContainer: BABYLON.ClusteredLightContainer;
|
||||
scene: BABYLON.Scene;
|
||||
graphicsQuality: number;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.type = args.type;
|
||||
const def = getAccessoryDef(this.type);
|
||||
this.options = convertRawOptions(def.options.schema, args.options, { files: [] });
|
||||
this.sr = args.sr;
|
||||
this.getIsSrReady = args.getIsSrReady;
|
||||
this.lightContainer = args.lightContainer;
|
||||
this.scene = args.scene;
|
||||
this.graphicsQuality = args.graphicsQuality;
|
||||
this.root = new BABYLON.TransformNode(`accessory_${args.id}_${args.type}`, this.scene);
|
||||
this.root.position = args.position;
|
||||
this.root.rotation = args.rotation;
|
||||
}
|
||||
|
||||
public async load() {
|
||||
const def = getAccessoryDef(this.type);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/world/objects/${def.path(this.options)}.glb` : `/client-assets/world/objects/${camelToKebab(this.type)}/${camelToKebab(this.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.subRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.subRoot.parent = this.root;
|
||||
this.subRoot.scaling.x = -1;
|
||||
this.subRoot.scaling = this.subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of subRootMesh.getChildren()) {
|
||||
if (m.parent === subRootMesh) {
|
||||
m.parent = this.subRoot;
|
||||
}
|
||||
}
|
||||
|
||||
subRootMesh.dispose();
|
||||
|
||||
this.registerMeshes(this.subRoot.getChildMeshes());
|
||||
|
||||
this.model = new ModelExplorer(this.subRoot);
|
||||
|
||||
this.instance = await def.createInstance({
|
||||
scene: this.scene,
|
||||
sr: {
|
||||
updateMesh: (mesh) => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.updateMesh(mesh);
|
||||
},
|
||||
reset: () => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
},
|
||||
fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps),
|
||||
},
|
||||
lc: this.lightContainer,
|
||||
root: this.root,
|
||||
options: this.options,
|
||||
model: this.model!,
|
||||
timer: this.timer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
reloadModel: () => {
|
||||
this.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async reload() {
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.instance = null;
|
||||
this.model = null;
|
||||
this.subRoot?.dispose();
|
||||
this.root.removeChild(this.subRoot);
|
||||
this.scene.removeTransformNode(this.subRoot);
|
||||
|
||||
this.timer = new Timer();
|
||||
|
||||
await this.load();
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public optionsUpdated(options: Record<string, unknown>, key: string, value: any) {
|
||||
if (this.instance == null) return;
|
||||
this.options[key] = options[key]; // 参照を切れさせないようにプロパティ個別にmutate
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.instance.onOptionsUpdated?.([key, this.options[key]]);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.subRoot.dispose();
|
||||
this.root.dispose();
|
||||
this.scene.removeTransformNode(this.root);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { bolt_schema } from 'misskey-world/src/avatars/accessories/bolt.schema.js';
|
||||
import { defineAccessory } from '../accessory.js';
|
||||
|
||||
export const bolt = defineAccessory(bolt_schema, {
|
||||
createInstance: ({ model, options }) => {
|
||||
const material = model.findMaterial('__X_BOLT__');
|
||||
|
||||
const applyMat = () => {
|
||||
material.albedoColor = new BABYLON.Color3(options.mat.color[0], options.mat.color[1], options.mat.color[2]);
|
||||
material.roughness = options.mat.roughness;
|
||||
material.metallic = options.mat.metallic;
|
||||
};
|
||||
|
||||
applyMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'mat': applyMat(); break;
|
||||
}
|
||||
},
|
||||
dispose: () => {
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { mikan_schema } from 'misskey-world/src/avatars/accessories/mikan.schema.js';
|
||||
import { defineAccessory } from '../accessory.js';
|
||||
|
||||
export const mikan = defineAccessory(mikan_schema, {
|
||||
createInstance: ({ scene, root, sr }) => {
|
||||
return {
|
||||
dispose: () => {
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { mug_schema } from 'misskey-world/src/avatars/accessories/mug.schema.js';
|
||||
import { defineAccessory } from '../accessory.js';
|
||||
|
||||
export const mug = defineAccessory(mug_schema, {
|
||||
createInstance: ({ options, scene, root, sr, model }) => {
|
||||
const emitter = new BABYLON.TransformNode('emitter', scene);
|
||||
emitter.parent = root;
|
||||
emitter.position = new BABYLON.Vector3(0, cm(5), 0);
|
||||
const ps = new BABYLON.ParticleSystem('steamParticleSystem', 8, scene);
|
||||
ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/mug/steam.png');
|
||||
ps.emitter = emitter;
|
||||
ps.minEmitBox = new BABYLON.Vector3(cm(-1), 0, cm(-1));
|
||||
ps.maxEmitBox = new BABYLON.Vector3(cm(1), 0, cm(1));
|
||||
ps.minEmitPower = cm(10);
|
||||
ps.maxEmitPower = cm(12);
|
||||
ps.minLifeTime = 2;
|
||||
ps.maxLifeTime = 3;
|
||||
ps.addSizeGradient(0, cm(10), cm(12));
|
||||
ps.addSizeGradient(1, cm(18), cm(20));
|
||||
ps.direction1 = new BABYLON.Vector3(-0.3, 1, 0.3);
|
||||
ps.direction2 = new BABYLON.Vector3(0.3, 1, -0.3);
|
||||
ps.emitRate = 0.5;
|
||||
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
|
||||
ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3);
|
||||
ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2);
|
||||
ps.colorDead = new BABYLON.Color4(1, 1, 1, 0);
|
||||
ps.preWarmCycles = Math.random() * 1000;
|
||||
ps.start();
|
||||
sr.fixParticleSystem(ps);
|
||||
|
||||
const bodyMaterial = model.findMaterial('__X_MUG__');
|
||||
|
||||
const applyBodyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.bodyMat.color[0], options.bodyMat.color[1], options.bodyMat.color[2]);
|
||||
bodyMaterial.roughness = options.bodyMat.roughness;
|
||||
bodyMaterial.metallic = options.bodyMat.metallic;
|
||||
};
|
||||
|
||||
applyBodyMat();
|
||||
|
||||
const liquidMaterial = model.findMaterial('__X_LIQUID__');
|
||||
|
||||
const applyLiquidMat = () => {
|
||||
liquidMaterial.albedoColor = new BABYLON.Color3(options.liquidMat.color[0], options.liquidMat.color[1], options.liquidMat.color[2]);
|
||||
liquidMaterial.roughness = options.liquidMat.roughness;
|
||||
liquidMaterial.metallic = options.liquidMat.metallic;
|
||||
};
|
||||
|
||||
applyLiquidMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'bodyMat': applyBodyMat(); break;
|
||||
case 'liquidMat': applyLiquidMat(); break;
|
||||
}
|
||||
},
|
||||
dispose: () => {
|
||||
ps.stop();
|
||||
emitter.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { mug } from './accessories/mug.js';
|
||||
import { mikan } from './accessories/mikan.js';
|
||||
import { bolt } from './accessories/bolt.js';
|
||||
import type { AvatarAccessoryDef } from './accessory.js';
|
||||
|
||||
export const AVATAR_ACCESSORY_DEFS = [
|
||||
mug,
|
||||
mikan,
|
||||
bolt,
|
||||
] as AvatarAccessoryDef[];
|
||||
|
||||
export function getAccessoryDef(type: string): AvatarAccessoryDef {
|
||||
const def = AVATAR_ACCESSORY_DEFS.find(x => x.id === type) as AvatarAccessoryDef | undefined;
|
||||
if (def == null) {
|
||||
throw new Error(`Unrecognized accessory type: ${type}`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { ModelExplorer, type Timer } from '../utility.js';
|
||||
import type { AccessorySchemaDef } from 'misskey-world/src/avatars/accessory.js';
|
||||
import type { OptionsSchema } from 'misskey-world/src/mono.js';
|
||||
import type { ConvertedOptions, GetConvertedOptionsSchemaValues } from '../mono.js';
|
||||
|
||||
export type AvatarAccessoryInstance<Options = any> = {
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export type SnapshotRenderingHelperWrapper = {
|
||||
updateMesh: (meshes: BABYLON.Mesh[]) => void;
|
||||
reset: () => void;
|
||||
fixParticleSystem: (ps: BABYLON.ParticleSystem) => void;
|
||||
};
|
||||
|
||||
export type AvatarAccessoryDef<Schema extends AccessorySchemaDef = AccessorySchemaDef> = Schema & {
|
||||
path?: (options: string extends keyof Schema['options']['schema'] ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<Schema['options']['schema']>>) => string;
|
||||
createInstance: (args: {
|
||||
scene: BABYLON.Scene;
|
||||
// TODO: snapshot renderingの関心を隠蔽した方が綺麗かもしれない
|
||||
// 例えばmaterialUpdatedというメソッドを用意して内部的にresetを呼ぶなど
|
||||
sr: SnapshotRenderingHelperWrapper;
|
||||
lc: BABYLON.ClusteredLightContainer | null;
|
||||
root: BABYLON.TransformNode;
|
||||
options: string extends keyof Schema['options']['schema'] ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<Schema['options']['schema']>>;
|
||||
model: ModelExplorer;
|
||||
timer: Timer;
|
||||
graphicsQuality: number;
|
||||
reloadModel: () => void;
|
||||
}) => AvatarAccessoryInstance<string extends keyof Schema['options']['schema'] ? ConvertedOptions : GetConvertedOptionsSchemaValues<Schema['options']['schema']>> | Promise<AvatarAccessoryInstance<Schema['options']['schema'] extends undefined ? ConvertedOptions : GetConvertedOptionsSchemaValues<Schema['options']['schema']>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
|
||||
};
|
||||
|
||||
export function defineAccessorySchema<const OpSc extends OptionsSchema>(def: AccessorySchemaDef<OpSc>): AccessorySchemaDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
||||
export function defineAccessory<const Schema extends AccessorySchemaDef<any>>(schema: Schema, def: Pick<AvatarAccessoryDef<Schema>, 'path' | 'createInstance'>): AvatarAccessoryDef<Schema> {
|
||||
return { ...schema, ...def };
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
|
||||
export function registerBabylonRuntime(): void {
|
||||
BABYLON.RegisterStandardEngineExtensions();
|
||||
BABYLON.RegisterStandardWebGPUEngineExtensions();
|
||||
BABYLON.RegisterAbstractEngineAlpha();
|
||||
BABYLON.RegisterAbstractEngineTexture();
|
||||
BABYLON.RegisterAbstractEngineCubeTexture();
|
||||
BABYLON.RegisterAbstractEngineQuery();
|
||||
BABYLON.RegisterAbstractEngineTextureSelector();
|
||||
BABYLON.RegisterAbstractEngineTimeQuery();
|
||||
BABYLON.RegisterAbstractEngineViews();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineRawTexture();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineReadTexture();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineCubeTexture();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineRenderTargetCube();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineQuery();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineDynamicTexture();
|
||||
BABYLON.RegisterBufferAlign();
|
||||
BABYLON.RegisterCubeTexture();
|
||||
BABYLON.RegisterStandardMaterial();
|
||||
BABYLON.RegisterRay();
|
||||
BABYLON.RegisterAnimation();
|
||||
BABYLON.RegisterAnimatable();
|
||||
BABYLON.RegisterCollisionCoordinator();
|
||||
BABYLON.RegisterInstancedMesh();
|
||||
BABYLON.RegisterPostProcessRenderPipelineManagerSceneComponent(
|
||||
BABYLON.PostProcessRenderPipelineManager,
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// structredCloneが遅いため
|
||||
// SEE: http://var.blog.jp/archives/86038606.html
|
||||
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||
|
||||
export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
|
||||
|
||||
export function deepClone<T extends Cloneable>(x: T): T {
|
||||
if (typeof x === 'object') {
|
||||
if (x === null) return x;
|
||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||
const obj = {} as Record<string | number | symbol, Cloneable>;
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||
}
|
||||
return obj as T;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
@@ -1,597 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import Hls from 'hls.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { RecyvlingTextGrid, Timer, createPlaneUvMapper, randomRange } from './utility.js';
|
||||
import { TIME_MAP } from './utility.js';
|
||||
import { EngineBase } from './EngineBase.js';
|
||||
|
||||
const SNAPSHOT_RENDERING = false; // 実験的
|
||||
const USE_GLOW = true; // ドローコールが増えて重い
|
||||
const IN_WEB_WORKER = typeof window === 'undefined';
|
||||
|
||||
export class WorldEngine extends EngineBase<{
|
||||
'playSfxUrl': (ctx: {
|
||||
url: string;
|
||||
options: {
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
};
|
||||
}) => void;
|
||||
'loadingProgress': (ctx: { progress: number }) => void;
|
||||
}> {
|
||||
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator;
|
||||
public camera: BABYLON.UniversalCamera;
|
||||
private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜
|
||||
private envMap: BABYLON.CubeTexture;
|
||||
public lightContainer: BABYLON.ClusteredLightContainer;
|
||||
public sr: BABYLON.SnapshotRenderingHelper;
|
||||
private gl: BABYLON.GlowLayer | null = null;
|
||||
private textMaterial: BABYLON.StandardMaterial;
|
||||
private translucentTextMaterial: BABYLON.StandardMaterial;
|
||||
private reflectionProbe: BABYLON.ReflectionProbe;
|
||||
public timer: Timer = new Timer();
|
||||
public isSitting = false;
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
}) {
|
||||
super({
|
||||
engine: options.engine,
|
||||
fps: null,
|
||||
});
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.scene.autoClear = false;
|
||||
//this.scene.autoClearDepthAndStencil = false;
|
||||
this.scene.skipPointerMovePicking = true;
|
||||
this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため
|
||||
this.scene.gravity = new BABYLON.Vector3(0, -0.1, 0).scale(WORLD_SCALE);
|
||||
|
||||
this.sr = new BABYLON.SnapshotRenderingHelper(this.scene);
|
||||
|
||||
const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(50000) }, this.scene);
|
||||
const skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.scene);
|
||||
skyboxMat.backFaceCulling = false;
|
||||
skyboxMat.disableLighting = true;
|
||||
skybox.material = skyboxMat;
|
||||
skybox.infiniteDistance = true;
|
||||
|
||||
this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP];
|
||||
//this.time = TIME_MAP[12 as keyof typeof TIME_MAP];
|
||||
|
||||
if (this.time === 0) {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
} else if (this.time === 1) {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.68, 0.66);
|
||||
} else {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.48, 0.5, 0.6);
|
||||
}
|
||||
this.scene.ambientColor = new BABYLON.Color3(0.9, 0.9, 0.9);
|
||||
|
||||
this.envMap = BABYLON.CubeTexture.CreateFromPrefilteredData(this.time === 2 ? '/client-assets/room/outdoor-night.env' : '/client-assets/room/outdoor-day.env', this.scene);
|
||||
//this.envMap.level = 1;
|
||||
this.envMap.level = 0;
|
||||
|
||||
this.scene.collisionsEnabled = true;
|
||||
|
||||
this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(cm(0), cm(250), cm(3000)), this.scene);
|
||||
this.camera.attachControl(this.canvas);
|
||||
this.camera.minZ = cm(1);
|
||||
this.camera.maxZ = cm(100000);
|
||||
this.camera.fov = 1;
|
||||
this.camera.ellipsoid = new BABYLON.Vector3(cm(15), cm(65), cm(15));
|
||||
this.camera.checkCollisions = true;
|
||||
this.camera.applyGravity = true;
|
||||
this.camera.needMoveForGravity = true;
|
||||
this.camera.keysUp.push(87); // W
|
||||
this.camera.keysDown.push(83); // S
|
||||
this.camera.keysLeft.push(65); // A
|
||||
this.camera.keysRight.push(68); // D
|
||||
const normalSpeed = 0.03 * WORLD_SCALE;
|
||||
this.camera.speed = normalSpeed;
|
||||
this.scene.onKeyboardObservable.add((kbInfo) => {
|
||||
switch (kbInfo.type) {
|
||||
case BABYLON.KeyboardEventTypes.KEYDOWN:
|
||||
if (kbInfo.event.key === 'Shift') {
|
||||
this.camera.speed = normalSpeed * 4;
|
||||
}
|
||||
break;
|
||||
case BABYLON.KeyboardEventTypes.KEYUP:
|
||||
if (kbInfo.event.key === 'Shift') {
|
||||
this.camera.speed = normalSpeed;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
//this.scene.activeCamera = this.camera;
|
||||
|
||||
//this.reflectionProbe = new BABYLON.ReflectionProbe('rp', 512, this.scene, true, true, false);
|
||||
//this.reflectionProbe.refreshRate = 60;
|
||||
|
||||
//const mainLight = new BABYLON.PointLight('mainLight', new BABYLON.Vector3(0, cm(300), 0), this.scene);
|
||||
//mainLight.intensity = 10000000;
|
||||
//mainLight.radius = cm(1000);
|
||||
//mainLight.diffuse = new BABYLON.Color3(1, 1, 1);
|
||||
|
||||
const ambientLight1 = new BABYLON.HemisphericLight('ambientLight1', new BABYLON.Vector3(0, 1, 0), this.scene);
|
||||
ambientLight1.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
ambientLight1.intensity = 1;
|
||||
const ambientLight2 = new BABYLON.HemisphericLight('ambientLight2', new BABYLON.Vector3(0, -1, 0), this.scene);
|
||||
ambientLight2.diffuse = new BABYLON.Color3(0.8, 0.9, 1.0);
|
||||
ambientLight2.intensity = 1;
|
||||
//ambientLight.intensity = 0;
|
||||
|
||||
const sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0, -1, 0), this.scene);
|
||||
sunLight.position = new BABYLON.Vector3(cm(0), cm(10000), cm(0));
|
||||
sunLight.diffuse = this.time === 0 ? new BABYLON.Color3(1.0, 1.0, 1.0) : this.time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
sunLight.intensity = this.time === 0 ? 2 : this.time === 1 ? 0.5 : 0.25;
|
||||
sunLight.shadowMinZ = cm(1000);
|
||||
sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(4096, sunLight);
|
||||
this.shadowGeneratorForSunLight.forceBackFacesOnly = true;
|
||||
this.shadowGeneratorForSunLight.bias = 0.0001;
|
||||
this.shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
|
||||
this.shadowGeneratorForSunLight.usePoissonSampling = true;
|
||||
//this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60;
|
||||
|
||||
this.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene);
|
||||
|
||||
if (USE_GLOW) {
|
||||
this.gl = new BABYLON.GlowLayer('glow', this.scene, {
|
||||
//mainTextureFixedSize: 512,
|
||||
blurKernelSize: 64,
|
||||
});
|
||||
this.gl.intensity = 0.5;
|
||||
this.gl.addExcludedMesh(skybox);
|
||||
this.scene.setRenderingAutoClearDepthStencil(this.gl.renderingGroupId, false);
|
||||
|
||||
if (SNAPSHOT_RENDERING) {
|
||||
this.sr.updateMeshesForEffectLayer(this.gl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async init() {
|
||||
await this.loadEnvModel();
|
||||
|
||||
if (SNAPSHOT_RENDERING) {
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
this.inputs.on('keydown', (ev) => {
|
||||
});
|
||||
|
||||
this.inputs.on('wheel', (ev) => {
|
||||
this.camera.fov += ev.deltaY * 0.001;
|
||||
this.camera.fov = Math.max(0.25, Math.min(1, this.camera.fov));
|
||||
});
|
||||
|
||||
this.inputs.on('click', (ev) => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private async loadEnvModel() {
|
||||
const envObj = await BABYLON.ImportMeshAsync('/client-assets/world/lobby/default.glb', this.scene);
|
||||
envObj.meshes[0].scaling = envObj.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
envObj.meshes[0].bakeCurrentTransformIntoVertices();
|
||||
for (const mesh of envObj.meshes) {
|
||||
if (mesh.name === '__root__') continue;
|
||||
if (mesh.name.includes('__COLLISION__')) {
|
||||
mesh.checkCollisions = true;
|
||||
mesh.isVisible = false;
|
||||
}
|
||||
if (this.reflectionProbe != null) {
|
||||
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.reflectionProbe.cubeTexture;
|
||||
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).realTimeFiltering = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.textMaterial = new BABYLON.StandardMaterial('textMaterial', this.scene);
|
||||
this.textMaterial.diffuseTexture = new BABYLON.Texture('/client-assets/world/chars.png', this.scene, false, false);
|
||||
this.textMaterial.diffuseTexture.hasAlpha = true;
|
||||
this.textMaterial.disableLighting = true;
|
||||
this.textMaterial.transparencyMode = BABYLON.Material.MATERIAL_ALPHABLEND;
|
||||
this.textMaterial.useAlphaFromDiffuseTexture = true;
|
||||
this.textMaterial.freeze();
|
||||
|
||||
this.translucentTextMaterial = this.textMaterial.clone('translucentTextMaterial');
|
||||
this.translucentTextMaterial.alpha = 0.25;
|
||||
|
||||
{
|
||||
const objet = envObj.meshes.find(m => m.name.includes('__OBJET__'));
|
||||
objet.rotation = objet.rotationQuaternion.toEulerAngles();
|
||||
objet.rotationQuaternion = null;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 5000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
objet.animations = [anim];
|
||||
this.scene.beginAnimation(objet, 0, 5000, true);
|
||||
}
|
||||
|
||||
{
|
||||
const ring = envObj.meshes.find(m => m.name.includes('__LED_RING__'));
|
||||
ring.rotation = ring.rotationQuaternion.toEulerAngles();
|
||||
ring.rotationQuaternion = null;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 5000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
ring.animations = [anim];
|
||||
this.scene.beginAnimation(ring, 0, 5000, true);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_1__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 256, {
|
||||
meshFlipped: true,
|
||||
material: this.textMaterial,
|
||||
});
|
||||
|
||||
text.write('Wellcome to Misskey World!');
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
const texts = [
|
||||
'Wellcome to Misskey World!',
|
||||
'Enjoy your stay!',
|
||||
'Feel free to look around!',
|
||||
'This is a virtual space for Misskey users!',
|
||||
//'You can chat, play games, and more!',
|
||||
//'Check out the bulletin board for announcements',
|
||||
'Have a nice day with Misskey!',
|
||||
'MAINTENANCE will begin at 9:00 A.M.',
|
||||
];
|
||||
|
||||
let currentTextIndex = 1;
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const textToShow = texts[currentTextIndex];
|
||||
currentTextIndex = (currentTextIndex + 1) % texts.length;
|
||||
text.writeWithAnimation(textToShow);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_2__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 256, {
|
||||
meshFlipped: true,
|
||||
material: this.translucentTextMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
messageRingRoot.rotation.x = Math.PI / 2;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
text.write(Date.now().toString());
|
||||
}, 10);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 64, {
|
||||
material: this.textMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: (Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
text.write(`${hours}:${minutes}:${seconds}`);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_2__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 64, {
|
||||
material: this.textMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const now = new Date();
|
||||
const years = now.getFullYear().toString();
|
||||
const months = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const days = now.getDate().toString().padStart(2, '0');
|
||||
text.write(`${years}/${months}/${days}`);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const sphereRoot = new BABYLON.TransformNode('', this.scene);
|
||||
sphereRoot.position = new BABYLON.Vector3(cm(0), cm(1000 + (100 * i)), cm(0));
|
||||
const rotation = Math.random() * Math.PI * 2;
|
||||
const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(50, 300)), segments: 16 }, this.scene);
|
||||
sphere.parent = sphereRoot;
|
||||
sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(2000, 7000)));
|
||||
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
const color = tinycolor({ h: Math.random() * 360, s: 1, l: 0.5 }).toRgb();
|
||||
mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255);
|
||||
mat.disableLighting = true;
|
||||
this.gl?.addExcludedMesh(sphere);
|
||||
sphere.material = mat;
|
||||
|
||||
const speed = randomRange(5000, 30000);
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: rotation },
|
||||
{ frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) },
|
||||
]);
|
||||
sphereRoot.animations = [anim];
|
||||
this.scene.beginAnimation(sphereRoot, 0, speed, true);
|
||||
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(sphere);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const sphereRoot = new BABYLON.TransformNode('', this.scene);
|
||||
sphereRoot.position = new BABYLON.Vector3(cm(0), cm(randomRange(-5000, 5000)), cm(0));
|
||||
const rotation = Math.random() * Math.PI * 2;
|
||||
const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(500, 3000)), segments: 16 }, this.scene);
|
||||
sphere.parent = sphereRoot;
|
||||
sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(10000, 15000)));
|
||||
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
const color = tinycolor({ h: Math.random() * 360, s: randomRange(0, 1), l: randomRange(0.75, 1) }).toRgb();
|
||||
mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255);
|
||||
mat.disableLighting = true;
|
||||
this.gl?.addExcludedMesh(sphere);
|
||||
sphere.material = mat;
|
||||
|
||||
const speed = randomRange(10000, 100000);
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: rotation },
|
||||
{ frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) },
|
||||
]);
|
||||
sphereRoot.animations = [anim];
|
||||
this.scene.beginAnimation(sphereRoot, 0, speed, true);
|
||||
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(sphere);
|
||||
}
|
||||
|
||||
//const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(10) }, this.scene);
|
||||
|
||||
const adsCountCol = 4;
|
||||
const adsCountRow = 2;
|
||||
for (let j = 0; j < adsCountRow; j++) {
|
||||
for (let i = 0; i < adsCountCol; i++) {
|
||||
const adRoot = new BABYLON.TransformNode(`ad_${j}_${i}_root`, this.scene);
|
||||
adRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (1000 * j)), cm(0));
|
||||
const rotation = (i / adsCountCol) * Math.PI * 2;
|
||||
const adMesh = BABYLON.MeshBuilder.CreatePlane(`ad_${j}_${i}`, { width: cm(1000), height: cm(700) }, this.scene);
|
||||
adMesh.parent = adRoot;
|
||||
adMesh.position = new BABYLON.Vector3(cm(0), cm(0), cm(7500));
|
||||
|
||||
const tex = new BABYLON.Texture('/client-assets/world/lobby/dummy-ads/angry_ai.png', this.scene);
|
||||
const adMat = new BABYLON.StandardMaterial(`ad_${j}_${i}_mat`, this.scene);
|
||||
adMat.emissiveTexture = tex;
|
||||
adMat.disableLighting = true;
|
||||
adMesh.material = adMat;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: rotation },
|
||||
{ frame: 15000, value: j % 2 === 0 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) },
|
||||
]);
|
||||
adRoot.animations = [anim];
|
||||
this.scene.beginAnimation(adRoot, 0, 15000, true);
|
||||
}
|
||||
}
|
||||
|
||||
const worldRingH = envObj.meshes.find(m => m.name.includes('__WORLD_RING_H__'));
|
||||
const worldRingM = envObj.meshes.find(m => m.name.includes('__WORLD_RING_M__'));
|
||||
|
||||
worldRingH.material.reflectionTexture = null;
|
||||
worldRingM.material.reflectionTexture = null;
|
||||
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(worldRingH);
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(worldRingM);
|
||||
|
||||
worldRingH.rotation = worldRingH.rotationQuaternion.toEulerAngles();
|
||||
worldRingM.rotation = worldRingM.rotationQuaternion.toEulerAngles();
|
||||
worldRingH.rotationQuaternion = null;
|
||||
worldRingM.rotationQuaternion = null;
|
||||
|
||||
const _1h = 1000 * 60 * 60;
|
||||
const _12h = _1h * 12;
|
||||
const _7days = _1h * 24 * 7;
|
||||
const _30days = _1h * 24 * 30;
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const time = Date.now();
|
||||
worldRingH.rotation.x = ((time % _12h) / _12h) * Math.PI * 2;
|
||||
worldRingM.rotation.y = -(((time % _1h) / _1h) * Math.PI);
|
||||
}, 100);
|
||||
|
||||
const screenMeshes = envObj.meshes.filter(m => m.name.includes('__SCREEN__'));
|
||||
const screenMaterial = screenMeshes[0].material as BABYLON.PBRMaterial;
|
||||
|
||||
const videoEl = document.createElement('video');
|
||||
videoEl.crossOrigin = 'anonymous';
|
||||
|
||||
const hls = new Hls();
|
||||
hls.loadSource('https://tvs.misskey.io/official/hq-beta/ts:abr.m3u8');
|
||||
hls.attachMedia(videoEl);
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
const tex = new BABYLON.VideoTexture('', videoEl, this.scene, true, true);
|
||||
tex.level = 0.5;
|
||||
tex.video.loop = true;
|
||||
tex.video.volume = 0.25;
|
||||
tex.video.muted = true;
|
||||
|
||||
screenMaterial.albedoColor = new BABYLON.Color3(0, 0, 0);
|
||||
screenMaterial.emissiveTexture = tex;
|
||||
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
|
||||
tex.onLoadObservable.addOnce(() => {
|
||||
tex.video.play();
|
||||
for (const mesh of screenMeshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) continue;
|
||||
//normalizeUvToSquare(mesh);
|
||||
const updateUv = createPlaneUvMapper(mesh);
|
||||
if (tex == null) return;
|
||||
const srcAspect = tex.getSize().width / tex.getSize().height;
|
||||
const targetAspect = 16 / 9;
|
||||
updateUv(srcAspect, targetAspect, 'cover');
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
const emitter = new BABYLON.TransformNode('emitter', this.scene);
|
||||
emitter.position = new BABYLON.Vector3(0, cm(-1000), 0);
|
||||
const ps = new BABYLON.ParticleSystem('', 128, this.scene);
|
||||
ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/lava-lamp/bubble.png');
|
||||
ps.emitter = emitter;
|
||||
ps.isLocal = true;
|
||||
ps.minEmitBox = new BABYLON.Vector3(cm(-1000), 0, cm(-1000));
|
||||
ps.maxEmitBox = new BABYLON.Vector3(cm(1000), 0, cm(1000));
|
||||
ps.minEmitPower = cm(100);
|
||||
ps.maxEmitPower = cm(500);
|
||||
ps.minLifeTime = 30;
|
||||
ps.maxLifeTime = 30;
|
||||
ps.minSize = cm(30);
|
||||
ps.maxSize = cm(300);
|
||||
ps.direction1 = new BABYLON.Vector3(0, 1, 0);
|
||||
ps.direction2 = new BABYLON.Vector3(0, 1, 0);
|
||||
ps.emitRate = 1.5;
|
||||
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
|
||||
ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3);
|
||||
ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2);
|
||||
ps.colorDead = new BABYLON.Color4(1, 1, 1, 0);
|
||||
ps.preWarmCycles = Math.random() * 1000;
|
||||
ps.start();
|
||||
}
|
||||
|
||||
public sitChair(furnitureId: string) {
|
||||
this.isSitting = true;
|
||||
this.fixedCamera.parent = this.objectMeshs.get(furnitureId);
|
||||
this.fixedCamera.position = new BABYLON.Vector3(0, cm(120), 0);
|
||||
this.fixedCamera.rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
this.scene.activeCamera = this.fixedCamera;
|
||||
this.selectFurniture(null);
|
||||
}
|
||||
|
||||
public standUp() {
|
||||
this.isSitting = false;
|
||||
this.scene.activeCamera = this.camera;
|
||||
this.fixedCamera.parent = null;
|
||||
}
|
||||
|
||||
private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) {
|
||||
this.emit('playSfxUrl', { url, options });
|
||||
}
|
||||
|
||||
public resize() {
|
||||
this.engine.resize(true);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.timer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class MessageRing {
|
||||
constructor(mesh: BABYLON.Mesh, scene: BABYLON.Scene, options: { material: BABYLON.StandardMaterial; repeatSeparator: string; }) {
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 64, {
|
||||
material: this.textMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: (Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
text.write(`${hours}:${minutes}:${seconds}`);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// ランダムな文字列が生成できればなんでも良い(時系列でソートできるなら尚良)が、とりあえずaidの実装を拝借
|
||||
|
||||
const TIME2000 = 946684800000;
|
||||
let counter = Math.floor(Math.random() * 10000);
|
||||
|
||||
function getTime(time: number): string {
|
||||
time = time - TIME2000;
|
||||
if (time < 0) time = 0;
|
||||
|
||||
return time.toString(36).padStart(8, '0');
|
||||
}
|
||||
|
||||
function getNoise(): string {
|
||||
return counter.toString(36).padStart(2, '0').slice(-2);
|
||||
}
|
||||
|
||||
export function genId(): string {
|
||||
counter++;
|
||||
return getTime(Date.now()) + getNoise();
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { OptionsSchema, NumberOptionSchema, BooleanOptionSchema, StringOptionSchema, ColorOptionSchema, MaterialOptionSchema, LightOptionSchema, EnumOptionSchema, RangeOptionSchema, ImageOptionSchema, SeedOptionSchema } from 'misskey-world/src/mono.js';
|
||||
|
||||
export type RawOptions = Record<string, unknown> & {
|
||||
readonly __brand: unique symbol;
|
||||
};
|
||||
|
||||
export type ConvertedOptions = Record<string, unknown> & {
|
||||
readonly __brand: unique symbol;
|
||||
};
|
||||
|
||||
type RawImageValue<Presets extends string = string> = { type: Presets | null | '_custom_'; driveFileId?: string | null; fit?: 'cover' | 'contain' | 'stretch'; rotation?: 0 | 1 | 2 | 3; };
|
||||
|
||||
type ConvertedImageValue<Presets extends string = string> = { type: Presets | null | '_custom_'; custom?: { url: string; } | null; fit?: 'cover' | 'contain' | 'stretch'; rotation?: 0 | 1 | 2 | 3; };
|
||||
export type GetConvertedOptionsSchemaValues<T extends OptionsSchema> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends NumberOptionSchema ? number :
|
||||
T[K] extends BooleanOptionSchema ? boolean :
|
||||
T[K] extends StringOptionSchema ? string :
|
||||
T[K] extends ColorOptionSchema ? [number, number, number] :
|
||||
T[K] extends MaterialOptionSchema ? { color: [number, number, number]; metallic: number; roughness: number; } :
|
||||
T[K] extends LightOptionSchema ? { color: [number, number, number]; brightness: number; } :
|
||||
T[K] extends EnumOptionSchema ? T[K]['enum'][number]['value'] :
|
||||
T[K] extends RangeOptionSchema ? number :
|
||||
T[K] extends ImageOptionSchema ? ConvertedImageValue<T[K]['presets'][number]['value']> :
|
||||
T[K] extends SeedOptionSchema ? number :
|
||||
never;
|
||||
};
|
||||
|
||||
export function convertRawOptions<OpSc extends OptionsSchema>(schema: OpSc, raw: RawOptions, attachments: { files: { id: string; url: string; }[] }): ConvertedOptions {
|
||||
const converted = {} as ConvertedOptions;
|
||||
for (const record of Object.entries(schema)) {
|
||||
const k = record[0];
|
||||
const v = raw[k];
|
||||
if (record[1].type === 'image') {
|
||||
const _v = v as unknown as RawImageValue;
|
||||
const file = _v.type === '_custom_' ? attachments.files.find(f => f.id === _v.driveFileId) : null;
|
||||
if (file != null && file.url.startsWith('http://syu-win.local:3000/')) { // debug
|
||||
file.url = file.url.replace('http://syu-win.local:3000/', 'https://local-mi.syuilo.dev/');
|
||||
}
|
||||
|
||||
converted[k] = { type: _v.type, custom: file != null ? { url: file.url } : null, fit: _v.fit, rotation: _v.rotation } satisfies ConvertedImageValue;
|
||||
} else {
|
||||
converted[k] = v;
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { camelToKebab, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { scaleMorph, Timer } from '../utility.js';
|
||||
import { convertRawOptions, type ConvertedOptions, type RawOptions } from '../mono.js';
|
||||
import { getFurnitureDef } from './furniture-defs.js';
|
||||
import { ModelManager, SYSTEM_MESH_NAMES } from './utility.js';
|
||||
import type { RoomFurnitureInstance } from './furniture.js';
|
||||
import type { RoomAttachments } from 'misskey-world/src/room/type.js';
|
||||
|
||||
function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boolean) {
|
||||
const excludeMeshes = root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)));
|
||||
|
||||
const childMeshes = root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed());
|
||||
|
||||
const toMerge = [] as BABYLON.Mesh[];
|
||||
for (const mesh of childMeshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mesh.hasInstances) continue;
|
||||
|
||||
if (mesh instanceof BABYLON.Mesh) {
|
||||
toMerge.push(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
if (toMerge.length <= 1) { // マージ対象が一つしかない状態でマージするのは単純に無駄なのと、babylonのバグが知らないけどなぜか法線が反転する
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const mesh of toMerge) {
|
||||
if (hasTexture) {
|
||||
if (mesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) {
|
||||
const vertexCount = mesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
mesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2);
|
||||
}
|
||||
if (mesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) {
|
||||
const vertexCount = mesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
mesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
export class FurnitureContainer {
|
||||
public id: string;
|
||||
public type: string;
|
||||
private options: ConvertedOptions;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode | null = null;
|
||||
public instance: RoomFurnitureInstance | null = null;
|
||||
public model: ModelManager | null = null;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private getIsSrReady: () => boolean;
|
||||
private lightContainer: BABYLON.ClusteredLightContainer;
|
||||
private graphicsQuality: number;
|
||||
private timer: Timer = new Timer();
|
||||
private sitChair: () => void = () => {};
|
||||
public boundingBox: {
|
||||
min: BABYLON.Vector3;
|
||||
max: BABYLON.Vector3;
|
||||
} | null = null;
|
||||
|
||||
constructor(args: {
|
||||
id: string;
|
||||
type: string;
|
||||
options: RawOptions;
|
||||
roomAttachments: RoomAttachments;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
sr: BABYLON.SnapshotRenderingHelper;
|
||||
getIsSrReady: () => boolean;
|
||||
lightContainer: BABYLON.ClusteredLightContainer;
|
||||
scene: BABYLON.Scene;
|
||||
graphicsQuality: number;
|
||||
sitChair?: () => void;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.type = args.type;
|
||||
const def = getFurnitureDef(this.type);
|
||||
this.options = convertRawOptions(def.options.schema, args.options, args.roomAttachments);
|
||||
this.sr = args.sr;
|
||||
this.getIsSrReady = args.getIsSrReady;
|
||||
this.lightContainer = args.lightContainer;
|
||||
this.scene = args.scene;
|
||||
this.graphicsQuality = args.graphicsQuality;
|
||||
this.root = new BABYLON.TransformNode(`furniture_${args.id}_${args.type}`, this.scene);
|
||||
this.root.position = args.position;
|
||||
this.root.rotation = args.rotation;
|
||||
if (args.sitChair != null) this.sitChair = args.sitChair;
|
||||
}
|
||||
|
||||
public async load() {
|
||||
const def = getFurnitureDef(this.type);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/world/objects/${def.path(this.options)}.glb` : `/client-assets/world/objects/${camelToKebab(this.type)}/${camelToKebab(this.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// 不要なUVを掃除
|
||||
if (!def.hasTexture) {
|
||||
for (const m of loaderResult.meshes) {
|
||||
if (m.geometry != null) {
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (def.canPreMeshesMerging) {
|
||||
const merged = mergeMeshes(loaderResult.meshes, subRootMesh, def.hasTexture);
|
||||
if (merged != null) {
|
||||
merged.setParent(subRootMesh);
|
||||
merged.name = 'preMerged';
|
||||
|
||||
merged.material.freeze();
|
||||
if (merged.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of merged.material.subMaterials) {
|
||||
subMat.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 再帰的にする
|
||||
for (const m of loaderResult.transformNodes) {
|
||||
if (m.getChildren().length === 0) {
|
||||
m.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.subRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.subRoot.parent = this.root;
|
||||
this.subRoot.scaling.x = -1;
|
||||
this.subRoot.scaling = this.subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of subRootMesh.getChildren()) {
|
||||
if (m.parent === subRootMesh) {
|
||||
m.parent = this.subRoot;
|
||||
}
|
||||
}
|
||||
|
||||
subRootMesh.dispose();
|
||||
|
||||
this.registerMeshes(this.subRoot.getChildMeshes());
|
||||
|
||||
this.model = new ModelManager(this.subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m.name !== '__root__'), def.hasTexture, (meshes) => {
|
||||
this.registerMeshes(meshes);
|
||||
});
|
||||
|
||||
this.instance = await def.createInstance({
|
||||
scene: this.scene,
|
||||
sr: {
|
||||
updateMesh: (mesh) => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.updateMesh(mesh);
|
||||
},
|
||||
reset: () => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
},
|
||||
fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps),
|
||||
},
|
||||
lc: this.lightContainer,
|
||||
root: this.root,
|
||||
options: this.options,
|
||||
model: this.model!,
|
||||
id: this.id,
|
||||
timer: this.timer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
reloadModel: () => {
|
||||
this.reload();
|
||||
},
|
||||
sitChair: () => {
|
||||
this.sitChair();
|
||||
},
|
||||
stickyMarkerMeshUpdated: (mesh) => {
|
||||
// TODO
|
||||
//// stickyな子の位置を更新
|
||||
//if (mesh.name.includes('__TOP__')) {
|
||||
// mesh.unfreezeWorldMatrix();
|
||||
// mesh.computeWorldMatrix(true);
|
||||
// const updateChildStickyObjectPosition = (furnitureId: string) => {
|
||||
// const stickyFurnitureIds = Array.from(this.roomState.installedFurnitures.filter(o => o.sticky === furnitureId)).map(o => o.id);
|
||||
// for (const soid of stickyFurnitureIds) {
|
||||
// const soMesh = this.objectEntities.get(soid)!.rootMesh;
|
||||
// soMesh.unfreezeWorldMatrix();
|
||||
// for (const m of soMesh.getChildMeshes()) {
|
||||
// m.unfreezeWorldMatrix();
|
||||
// }
|
||||
// console.log(mesh.getAbsolutePosition().y);
|
||||
// soMesh.position.y = mesh.getAbsolutePosition().y;
|
||||
// updateChildStickyObjectPosition(soid);
|
||||
// }
|
||||
// };
|
||||
// updateChildStickyObjectPosition(args.id);
|
||||
//}
|
||||
},
|
||||
});
|
||||
|
||||
this.instance.onInited?.();
|
||||
|
||||
this.calcBoundingBox();
|
||||
}
|
||||
|
||||
public calcBoundingBox() {
|
||||
// TODO: モーフ最大適用後のサイズが取得されてしまうのを直す
|
||||
this.boundingBox = this.subRoot.getHierarchyBoundingVectors(true);
|
||||
}
|
||||
|
||||
public interact(iid: string | null = null) {
|
||||
if (this.instance == null) return;
|
||||
if (iid == null) {
|
||||
if (this.instance.primaryInteraction != null) {
|
||||
this.instance.interactions[this.instance.primaryInteraction].fn();
|
||||
}
|
||||
} else {
|
||||
this.instance.interactions[iid].fn();
|
||||
}
|
||||
}
|
||||
|
||||
public async reload() {
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.instance = null;
|
||||
this.model = null;
|
||||
this.subRoot?.dispose();
|
||||
this.root.removeChild(this.subRoot);
|
||||
this.scene.removeTransformNode(this.subRoot);
|
||||
|
||||
this.timer = new Timer();
|
||||
|
||||
await this.load();
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public optionsUpdated(options: RawOptions, key: string, value: any, roomAttachments: RoomAttachments) {
|
||||
if (this.instance == null) return;
|
||||
const def = getFurnitureDef(this.type);
|
||||
const convertedOptions = convertRawOptions(def.options.schema, options, roomAttachments);
|
||||
this.options[key] = convertedOptions[key]; // 参照を切れさせないようにプロパティ個別にmutate
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.instance.onOptionsUpdated?.([key, this.options[key]]);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.subRoot.dispose();
|
||||
this.root.dispose();
|
||||
this.scene.removeTransformNode(this.root);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,123 +0,0 @@
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from './utility.js';
|
||||
import type { RoomEngine } from './engine.js';
|
||||
|
||||
export abstract class EnvManager<T = any> {
|
||||
protected engine: RoomEngine;
|
||||
public abstract envMapIndoor: BABYLON.CubeTexture | null;
|
||||
public abstract maxCameraZ: number;
|
||||
private shadowGenerators: BABYLON.ShadowGenerator[] = [];
|
||||
protected isRoomLightOn = true;
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
abstract load(options: T): Promise<void>;
|
||||
abstract applyOptions(options: T): void;
|
||||
abstract setTime(time: number): void;
|
||||
abstract applyRoomLight(): void;
|
||||
|
||||
public turnOnRoomLight() {
|
||||
this.isRoomLightOn = true;
|
||||
this.applyRoomLight();
|
||||
}
|
||||
|
||||
public turnOffRoomLight() {
|
||||
this.isRoomLightOn = false;
|
||||
this.applyRoomLight();
|
||||
}
|
||||
|
||||
protected registerShadowGenerator(shadowGenerator: BABYLON.ShadowGenerator) {
|
||||
this.shadowGenerators.push(shadowGenerator);
|
||||
|
||||
const shadowMap = shadowGenerator.getShadowMap()!;
|
||||
shadowMap.refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
||||
|
||||
// https://forum.babylonjs.com/t/is-it-intentional-that-the-shadow-map-refresh-rate-is-ignored-under-fast-snapshot-rendering/63523
|
||||
const objectRenderer = shadowMap._objectRenderer;
|
||||
const originalShouldRender = objectRenderer.shouldRender.bind(objectRenderer);
|
||||
objectRenderer.shouldRender = function () {
|
||||
if (this._engine.snapshotRendering) {
|
||||
return this.refreshRate !== BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
||||
}
|
||||
|
||||
return originalShouldRender();
|
||||
};
|
||||
}
|
||||
|
||||
public addShadowCaster(mesh: BABYLON.AbstractMesh) {
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
shadowGen.addShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
public removeShadowCaster(mesh: BABYLON.AbstractMesh) {
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
shadowGen.removeShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
public async renderShadow() {
|
||||
this.engine.sr.disableSnapshotRendering();
|
||||
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
const shadowMap = shadowGen.getShadowMap()!;
|
||||
shadowMap.refreshRate = 1;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
const shadowMap = shadowGen.getShadowMap()!;
|
||||
shadowMap.refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
||||
}
|
||||
|
||||
this.engine.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
protected registerMeshes(meshes: BABYLON.AbstractMesh[]) {
|
||||
for (const mesh of meshes) {
|
||||
if (!this.engine.scene.meshes.includes(mesh)) this.engine.scene.addMesh(mesh);
|
||||
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) {
|
||||
mesh.isPickable = false;
|
||||
mesh.receiveShadows = false;
|
||||
mesh.isVisible = false;
|
||||
mesh.checkCollisions = false;
|
||||
if (mesh.name.includes('__COLLISION__')) {
|
||||
mesh.checkCollisions = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
mesh.isPickable = false;
|
||||
mesh.checkCollisions = false;
|
||||
if (mesh.material != null) {
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
subMat.reflectionTexture = this.envMapIndoor;
|
||||
}
|
||||
} else if (mesh.material instanceof BABYLON.PBRMaterial) {
|
||||
mesh.material.reflectionTexture = this.envMapIndoor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
shadowGen.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { CustomMadoriEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export class CustomMadoriEnvManager extends EnvManager<CustomMadoriEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private rootNode: BABYLON.TransformNode;
|
||||
private unitRootNodes: (BABYLON.TransformNode | null)[] = [];
|
||||
private floorRootNode: BABYLON.TransformNode | null = null;
|
||||
private wallRootNode: BABYLON.TransformNode | null = null;
|
||||
private floorMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private wallMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private wallBeamMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private pillarMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private ceilingMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private beamMesh: BABYLON.Mesh | null = null;
|
||||
private baseboardMesh: BABYLON.Mesh | null = null;
|
||||
private wallARootNode: BABYLON.TransformNode | null = null;
|
||||
private wallBRootNode: BABYLON.TransformNode | null = null;
|
||||
private skybox: BABYLON.Mesh | null = null;
|
||||
private skyboxMat: BABYLON.StandardMaterial | null = null;
|
||||
private roomLight: BABYLON.DirectionalLight | null = null;
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(3000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
|
||||
this.rootNode = new BABYLON.TransformNode('customMadoriRoot', this.engine.scene);
|
||||
//this.rootNode.scaling = new BABYLON.Vector3(WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
}
|
||||
|
||||
public async load(options: CustomMadoriEnvOptions) {
|
||||
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(3000) }, this.engine.scene);
|
||||
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene);
|
||||
this.skyboxMat.backFaceCulling = false;
|
||||
this.skyboxMat.disableLighting = true;
|
||||
this.skybox.material = this.skyboxMat;
|
||||
this.skybox.infiniteDistance = true;
|
||||
|
||||
this.roomLight = new BABYLON.DirectionalLight('env:RoomLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene);
|
||||
this.roomLight.position = new BABYLON.Vector3(0, cm(300), 0);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(500);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
this.applyRoomLight();
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.0005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
//shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
//shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01;
|
||||
this.registerShadowGenerator(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
for (const materialDef of options.flooringMaterials) {
|
||||
const mat = new BABYLON.PBRMaterial(`flooring_${materialDef.id}`, this.engine.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(...materialDef.color);
|
||||
mat.metallic = 0;
|
||||
mat.roughness = 1;
|
||||
|
||||
const texPath = materialDef.texture === 'wood' ? '/client-assets/room/textures/flooring-wood.png'
|
||||
: materialDef.texture === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
mat.albedoTexture = tex;
|
||||
}
|
||||
|
||||
//mat.freeze();
|
||||
this.floorMaterials[materialDef.id] = mat;
|
||||
}
|
||||
|
||||
for (const materialDef of options.wallMaterials) {
|
||||
const mat = new BABYLON.PBRMaterial(`wall_${materialDef.id}`, this.engine.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(...materialDef.color);
|
||||
mat.metallic = 0;
|
||||
mat.roughness = 1;
|
||||
|
||||
const texPath = materialDef.texture === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: materialDef.texture === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
mat.albedoTexture = tex;
|
||||
}
|
||||
|
||||
//mat.freeze();
|
||||
this.wallMaterials[materialDef.id] = mat;
|
||||
}
|
||||
|
||||
for (const materialDef of options.ceilingMaterials) {
|
||||
const mat = new BABYLON.PBRMaterial(`ceiling_${materialDef.id}`, this.engine.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(...materialDef.color);
|
||||
mat.metallic = 0;
|
||||
mat.roughness = 1;
|
||||
|
||||
const texPath = materialDef.texture === 'wood' ? '/client-assets/room/textures/ceiling-wood.png'
|
||||
: materialDef.texture === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
mat.albedoTexture = tex;
|
||||
}
|
||||
|
||||
//mat.freeze();
|
||||
this.ceilingMaterials[materialDef.id] = mat;
|
||||
}
|
||||
|
||||
this.loaderResult = await BABYLON.LoadAssetContainerAsync('/client-assets/room/envs/custom-madori/units.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(2000), cm(500), cm(2000));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
for (const m of this.meshes[0].getChildren()) {
|
||||
if (m.parent === this.meshes[0]) {
|
||||
m.parent = this.rootNode;
|
||||
}
|
||||
}
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
this.floorRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__FLOOR__'))!;
|
||||
this.wallRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL__'))!;
|
||||
this.beamMesh = this.loaderResult.meshes.find(m => m.name.includes('__BEAM__')) as BABYLON.Mesh;
|
||||
this.baseboardMesh = this.loaderResult.meshes.find(m => m.name.includes('__BASEBOARD__')) as BABYLON.Mesh;
|
||||
this.wallARootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_A__'))!;
|
||||
this.wallBRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_B__'))!;
|
||||
|
||||
const baseboardMaterial = findMaterial(this.rootNode, '__BASEBOARD__');
|
||||
//baseboardMaterial.metadata.disableEnvMap = true;
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
private createUnit(options: CustomMadoriEnvOptions, x: number, z: number) {
|
||||
function indexToPos(index: number): [number, number] {
|
||||
const z = Math.floor(index / options.dimension[0]);
|
||||
const x = index % options.dimension[0];
|
||||
return [x, z];
|
||||
}
|
||||
|
||||
function posToIndex(x: number, z: number): number {
|
||||
if (x < 0 || z < 0 || x >= options.dimension[0] || z >= options.dimension[1]) return -1;
|
||||
return x + (options.dimension[0] * z);
|
||||
}
|
||||
|
||||
const unitDef = options.units[posToIndex(x, z)];
|
||||
if (unitDef == null) return;
|
||||
|
||||
const unitZPositiveDef = options.units[posToIndex(x, z + 1)];
|
||||
const unitZNegativeDef = options.units[posToIndex(x, z - 1)];
|
||||
const unitXPositiveDef = options.units[posToIndex(x + 1, z)];
|
||||
const unitXNegativeDef = options.units[posToIndex(x - 1, z)];
|
||||
|
||||
const shiftedX = x - (options.dimension[0] / 2) + 0.5;
|
||||
|
||||
const unitRoot = new BABYLON.TransformNode(`unit_${x}_${z}`, this.engine.scene);
|
||||
unitRoot.parent = this.rootNode;
|
||||
unitRoot.position = new BABYLON.Vector3(cm(100) * shiftedX, 0, cm(100) * z);
|
||||
|
||||
const defaultFlooringMaterial = this.floorMaterials[options.flooringMaterials[0].id];
|
||||
const unitFloorRootNode = this.floorRootNode.clone(`unit_${x}_${z}_floor`, unitRoot)!;
|
||||
unitFloorRootNode.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
const flooringMesh = unitFloorRootNode.getChildMeshes().find(m => m.name.includes('__FLOOR__'));
|
||||
flooringMesh.material = unitDef.flooring?.material != null && this.floorMaterials[unitDef.flooring.material] != null ? this.floorMaterials[unitDef.flooring.material] : defaultFlooringMaterial;
|
||||
const defaultCeilingMaterial = this.ceilingMaterials[options.ceilingMaterials[0].id];
|
||||
const ceilingMesh = unitFloorRootNode.getChildMeshes().find(m => m.name.includes('__CEILING__'));
|
||||
ceilingMesh.material = unitDef.ceiling?.material != null && this.ceilingMaterials[unitDef.ceiling.material] != null ? this.ceilingMaterials[unitDef.ceiling.material] : defaultCeilingMaterial;
|
||||
const defaultWallMaterial = this.wallMaterials[options.wallMaterials[0].id];
|
||||
|
||||
const createWall = (dir: 'zPositive' | 'zNegative' | 'xPositive' | 'xNegative') => {
|
||||
const wallDef = unitDef.walls?.[dir] ?? {};
|
||||
const wallRootNode = this.wallRootNode.clone(`unit_${x}_${z}_wall_${dir}`, unitRoot)!;
|
||||
wallRootNode.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
|
||||
switch (dir) {
|
||||
case 'zPositive':
|
||||
wallRootNode.rotation = new BABYLON.Vector3(0, Math.PI, 0);
|
||||
wallRootNode.position = new BABYLON.Vector3(0, 0, cm(50));
|
||||
break;
|
||||
case 'zNegative':
|
||||
wallRootNode.position = new BABYLON.Vector3(0, 0, cm(-50));
|
||||
break;
|
||||
case 'xPositive':
|
||||
wallRootNode.rotation = new BABYLON.Vector3(0, -Math.PI / 2, 0);
|
||||
wallRootNode.position = new BABYLON.Vector3(cm(50), 0, 0);
|
||||
break;
|
||||
case 'xNegative':
|
||||
wallRootNode.rotation = new BABYLON.Vector3(0, Math.PI / 2, 0);
|
||||
wallRootNode.position = new BABYLON.Vector3(cm(-50), 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
const beamMesh = wallRootNode.getChildMeshes().find(m => m.name.includes('__BEAM__'));
|
||||
beamMesh.isVisible = wallDef.withBeam === true;
|
||||
|
||||
const baseboardMesh = wallRootNode.getChildMeshes().find(m => m.name.includes('__BASEBOARD__'));
|
||||
baseboardMesh.isVisible = wallDef.withBaseboard === true;
|
||||
|
||||
switch (wallDef.type) {
|
||||
case 'window': {
|
||||
const wallNode = this.wallBRootNode.clone('', wallRootNode)!;
|
||||
const wallMesh = wallNode.getChildMeshes().find(m => m.name.includes('__WALL__'))!;
|
||||
wallMesh.material = wallDef.material != null && this.wallMaterials[wallDef.material] != null ? this.wallMaterials[wallDef.material] : defaultWallMaterial;
|
||||
break;
|
||||
}
|
||||
case 'door': {
|
||||
//wallMeshOriginal = this.wallAMesh;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const wallNode = this.wallARootNode.clone('', wallRootNode)!;
|
||||
const wallMesh = wallNode.getChildMeshes().find(m => m.name.includes('__WALL__'))!;
|
||||
wallMesh.material = wallDef.material != null && this.wallMaterials[wallDef.material] != null ? this.wallMaterials[wallDef.material] : defaultWallMaterial;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (unitZPositiveDef == null) createWall('zPositive');
|
||||
if (unitZNegativeDef == null) createWall('zNegative');
|
||||
if (unitXPositiveDef == null) createWall('xPositive');
|
||||
if (unitXNegativeDef == null) createWall('xNegative');
|
||||
|
||||
for (const mesh of unitRoot.getChildMeshes()) {
|
||||
this.meshes.push(mesh);
|
||||
}
|
||||
|
||||
this.registerMeshes(unitRoot.getChildMeshes());
|
||||
|
||||
return unitRoot;
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
if (this.skyboxMat == null) return;
|
||||
|
||||
if (time === 0) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (time === 1) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
|
||||
if (this.sunLight != null) {
|
||||
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public applyRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.light.color);
|
||||
this.roomLight.intensity = 0.0005 * WORLD_SCALE * WORLD_SCALE * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0);
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025 + (0.575 * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0));
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = this.isRoomLightOn ? new BABYLON.Color3(0.5, 0.5, 0.5) : new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: CustomMadoriEnvOptions) {
|
||||
// TODO: 返り値をpromiseにしてちゃんとテクスチャが読み終わってからresolveする
|
||||
|
||||
for (const n of this.unitRootNodes) {
|
||||
if (n != null) n.dispose();
|
||||
}
|
||||
this.unitRootNodes = [];
|
||||
|
||||
for (let z = 0; z < options.dimension[1]; z++) {
|
||||
for (let x = 0; x < options.dimension[0]; x++) {
|
||||
const node = this.createUnit(options, x, z);
|
||||
this.unitRootNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
this.skybox?.dispose();
|
||||
this.skyboxMat?.dispose();
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
this.sunLight?.dispose();
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { JapaneseEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export class JapaneseEnvManager extends EnvManager<JapaneseEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private skybox: BABYLON.Mesh | null = null;
|
||||
private skyboxMat: BABYLON.StandardMaterial | null = null;
|
||||
private roomLight: BABYLON.SpotLight | null = null;
|
||||
private sunLight: BABYLON.DirectionalLight | null = null;
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(1000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public async load(options: JapaneseEnvOptions) {
|
||||
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.engine.scene);
|
||||
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene);
|
||||
this.skyboxMat.backFaceCulling = false;
|
||||
this.skyboxMat.disableLighting = true;
|
||||
this.skybox.material = this.skyboxMat;
|
||||
this.skybox.infiniteDistance = true;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('env:RoomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(300);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
this.applyRoomLight();
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.0005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
//shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
//shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01;
|
||||
this.registerShadowGenerator(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
this.sunLight = new BABYLON.DirectionalLight('env:SunLight', new BABYLON.Vector3(0.2, -1, -1), this.engine.scene);
|
||||
this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
|
||||
this.sunLight.shadowMinZ = cm(1000);
|
||||
this.sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.sunLight);
|
||||
shadowGeneratorForSunLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForSunLight.bias = 0.00001;
|
||||
shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForSunLight.usePoissonSampling = true;
|
||||
this.registerShadowGenerator(shadowGeneratorForSunLight);
|
||||
}
|
||||
|
||||
this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/japanese/japanese.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].scaling = this.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
|
||||
this.addShadowCaster(mesh);
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
if (this.skyboxMat == null) return;
|
||||
|
||||
if (time === 0) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (time === 1) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
|
||||
if (this.sunLight != null) {
|
||||
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public applyRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.light.color);
|
||||
this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0);
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025 + (0.575 * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0));
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = this.isRoomLightOn ? new BABYLON.Color3(0.5, 0.5, 0.5) : new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: JapaneseEnvOptions) {
|
||||
this.registerMeshes(this.meshes);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
this.skybox?.dispose();
|
||||
this.skyboxMat?.dispose();
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
this.sunLight?.dispose();
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { MuseumEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export class MuseumEnvManager extends EnvManager<MuseumEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private roomLight: BABYLON.DirectionalLight | null = null;
|
||||
private subRoomLights: BABYLON.SpotLight[] = [];
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(3000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public async load(options: MuseumEnvOptions) {
|
||||
this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/museum/museum.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(2000), cm(500), cm(2000));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].scaling = this.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
this.roomLight = new BABYLON.DirectionalLight('env:RoomLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene);
|
||||
this.roomLight.position = new BABYLON.Vector3(0, cm(300), 0);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(500);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.00001;
|
||||
shadowGeneratorForRoomLight.normalBias = 0.005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
//this.shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
this.registerShadowGenerator(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
for (const node of this.meshes.filter(mesh => mesh.name.includes('__LIGHT__'))) {
|
||||
const light = new BABYLON.SpotLight('env:SubRoomLight', node.position, new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene, true);
|
||||
light.range = cm(500);
|
||||
light.radius = cm(15);
|
||||
light.parent = this.meshes[0];
|
||||
this.engine.lightContainer.addLight(light);
|
||||
this.subRoomLights.push(light);
|
||||
}
|
||||
|
||||
this.applyRoomLight();
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
//this.addShadowCaster(mesh);
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
}
|
||||
|
||||
public applyRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.light.color);
|
||||
this.roomLight.intensity = 0.00005 * WORLD_SCALE * WORLD_SCALE * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0);
|
||||
for (const subLight of this.subRoomLights) {
|
||||
subLight.diffuse = new BABYLON.Color3(...this.engine.roomState.light.color);
|
||||
subLight.intensity = 20 * WORLD_SCALE * WORLD_SCALE * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0);
|
||||
}
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025 + (0.175 * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0));
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = this.isRoomLightOn ? new BABYLON.Color3(0.5, 0.5, 0.5) : new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: MuseumEnvOptions) {
|
||||
this.registerMeshes(this.meshes);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
for (const subLight of this.subRoomLights) {
|
||||
subLight.dispose();
|
||||
}
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY, treeClone } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { SimpleEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
// TODO: マテリアルは必要になるまで作成しないようにする
|
||||
|
||||
export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private rootNode: BABYLON.TransformNode;
|
||||
private wallRoots: Record<'zPositive' | 'zNegative' | 'xPositive' | 'xNegative', BABYLON.TransformNode>;
|
||||
private wallScalingContainers: Record<'zPositive' | 'zNegative' | 'xPositive' | 'xNegative', BABYLON.TransformNode>;
|
||||
private wallMaterials: Record<'zPositive' | 'zNegative' | 'xPositive' | 'xNegative', BABYLON.PBRMaterial>;
|
||||
private wallBeamMaterials: Record<'zPositive' | 'zNegative' | 'xPositive' | 'xNegative', BABYLON.PBRMaterial>;
|
||||
private pillarRoots: Record<'zp_xp' | 'zp_xn' | 'zn_xp' | 'zn_xn', BABYLON.TransformNode>;
|
||||
private pillarScalingContainers: Record<'zp_xp' | 'zp_xn' | 'zn_xp' | 'zn_xn', BABYLON.TransformNode>;
|
||||
private pillarMaterials: Record<'zp_xp' | 'zp_xn' | 'zn_xp' | 'zn_xn', BABYLON.PBRMaterial>;
|
||||
private ceilingMaterial: BABYLON.PBRMaterial;
|
||||
private floorMaterial: BABYLON.PBRMaterial;
|
||||
private skybox: BABYLON.Mesh | null = null;
|
||||
private skyboxMat: BABYLON.StandardMaterial | null = null;
|
||||
private roomLight: BABYLON.SpotLight | null = null;
|
||||
private sunLight: BABYLON.DirectionalLight | null = null;
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(1000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
|
||||
this.rootNode = new BABYLON.TransformNode('simpleEnvRoot', this.engine.scene);
|
||||
|
||||
this.wallRoots = {
|
||||
zPositive: new BABYLON.TransformNode('wallRootZPositive', this.engine.scene),
|
||||
zNegative: new BABYLON.TransformNode('wallRootZNegative', this.engine.scene),
|
||||
xPositive: new BABYLON.TransformNode('wallRootXPositive', this.engine.scene),
|
||||
xNegative: new BABYLON.TransformNode('wallRootXNegative', this.engine.scene),
|
||||
};
|
||||
this.wallRoots.zPositive.parent = this.rootNode;
|
||||
this.wallRoots.zPositive.position.z = cm(150);
|
||||
this.wallRoots.zPositive.rotation.y = Math.PI;
|
||||
this.wallRoots.zNegative.parent = this.rootNode;
|
||||
this.wallRoots.zNegative.position.z = -cm(150);
|
||||
this.wallRoots.xPositive.parent = this.rootNode;
|
||||
this.wallRoots.xPositive.position.x = cm(150);
|
||||
this.wallRoots.xPositive.rotation.y = -Math.PI / 2;
|
||||
this.wallRoots.xNegative.parent = this.rootNode;
|
||||
this.wallRoots.xNegative.position.x = -cm(150);
|
||||
this.wallRoots.xNegative.rotation.y = Math.PI / 2;
|
||||
|
||||
this.wallScalingContainers = {
|
||||
zPositive: new BABYLON.TransformNode('wallScalingContainerZPositive', this.engine.scene),
|
||||
zNegative: new BABYLON.TransformNode('wallScalingContainerZNegative', this.engine.scene),
|
||||
xPositive: new BABYLON.TransformNode('wallScalingContainerXPositive', this.engine.scene),
|
||||
xNegative: new BABYLON.TransformNode('wallScalingContainerXNegative', this.engine.scene),
|
||||
};
|
||||
for (const [k, v] of Object.entries(this.wallScalingContainers)) {
|
||||
v.parent = this.wallRoots[k as keyof typeof this.wallRoots];
|
||||
v.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
}
|
||||
|
||||
this.pillarRoots = {
|
||||
zp_xp: new BABYLON.TransformNode('pillarRootZpXp', this.engine.scene),
|
||||
zp_xn: new BABYLON.TransformNode('pillarRootZpXn', this.engine.scene),
|
||||
zn_xp: new BABYLON.TransformNode('pillarRootZnXp', this.engine.scene),
|
||||
zn_xn: new BABYLON.TransformNode('pillarRootZnXn', this.engine.scene),
|
||||
};
|
||||
this.pillarRoots.zp_xp.parent = this.rootNode;
|
||||
this.pillarRoots.zp_xp.position = new BABYLON.Vector3(cm(150), 0, cm(150));
|
||||
this.pillarRoots.zp_xp.rotation.y = -Math.PI / 2;
|
||||
this.pillarRoots.zp_xn.parent = this.rootNode;
|
||||
this.pillarRoots.zp_xn.position = new BABYLON.Vector3(-cm(150), 0, cm(150));
|
||||
this.pillarRoots.zp_xn.rotation.y = Math.PI;
|
||||
this.pillarRoots.zn_xp.parent = this.rootNode;
|
||||
this.pillarRoots.zn_xp.position = new BABYLON.Vector3(cm(150), 0, -cm(150));
|
||||
this.pillarRoots.zn_xp.rotation.y = 0;
|
||||
this.pillarRoots.zn_xn.parent = this.rootNode;
|
||||
this.pillarRoots.zn_xn.position = new BABYLON.Vector3(-cm(150), 0, -cm(150));
|
||||
this.pillarRoots.zn_xn.rotation.y = Math.PI / 2;
|
||||
|
||||
this.pillarScalingContainers = {
|
||||
zp_xp: new BABYLON.TransformNode('pillarScalingContainerZpXp', this.engine.scene),
|
||||
zp_xn: new BABYLON.TransformNode('pillarScalingContainerZpXn', this.engine.scene),
|
||||
zn_xp: new BABYLON.TransformNode('pillarScalingContainerZnXp', this.engine.scene),
|
||||
zn_xn: new BABYLON.TransformNode('pillarScalingContainerZnXn', this.engine.scene),
|
||||
};
|
||||
for (const [k, v] of Object.entries(this.pillarScalingContainers)) {
|
||||
v.parent = this.pillarRoots[k as keyof typeof this.pillarRoots];
|
||||
v.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
}
|
||||
|
||||
const wallMaterial = new BABYLON.PBRMaterial('wallMaterial', this.engine.scene);
|
||||
wallMaterial.albedoColor = new BABYLON.Color3(0.8, 0.8, 0.8);
|
||||
wallMaterial.roughness = 0.7;
|
||||
wallMaterial.metallic = 0;
|
||||
this.wallMaterials = {
|
||||
zPositive: wallMaterial.clone('wallZPositiveMaterial'),
|
||||
zNegative: wallMaterial.clone('wallZNegativeMaterial'),
|
||||
xPositive: wallMaterial.clone('wallXPositiveMaterial'),
|
||||
xNegative: wallMaterial.clone('wallXNegativeMaterial'),
|
||||
};
|
||||
|
||||
const beamMaterial = wallMaterial.clone('beamMaterial');
|
||||
this.wallBeamMaterials = {
|
||||
zPositive: beamMaterial.clone('beamZPositiveMaterial'),
|
||||
zNegative: beamMaterial.clone('beamZNegativeMaterial'),
|
||||
xPositive: beamMaterial.clone('beamXPositiveMaterial'),
|
||||
xNegative: beamMaterial.clone('beamXNegativeMaterial'),
|
||||
};
|
||||
|
||||
const pillarMaterial = wallMaterial.clone('pillarMaterial');
|
||||
this.pillarMaterials = {
|
||||
zp_xp: pillarMaterial.clone('pillarMaterialZpXp'),
|
||||
zp_xn: pillarMaterial.clone('pillarMaterialZpXn'),
|
||||
zn_xp: pillarMaterial.clone('pillarMaterialZnXp'),
|
||||
zn_xn: pillarMaterial.clone('pillarMaterialZnXn'),
|
||||
};
|
||||
|
||||
this.ceilingMaterial = new BABYLON.PBRMaterial('ceilingMaterial', this.engine.scene);
|
||||
this.ceilingMaterial.albedoColor = new BABYLON.Color3(0.8, 0.8, 0.8);
|
||||
this.ceilingMaterial.roughness = 0.7;
|
||||
this.ceilingMaterial.metallic = 0;
|
||||
this.floorMaterial = new BABYLON.PBRMaterial('floorMaterial', this.engine.scene);
|
||||
this.floorMaterial.albedoColor = new BABYLON.Color3(0.8, 0.8, 0.8);
|
||||
this.floorMaterial.roughness = 0.7;
|
||||
this.floorMaterial.metallic = 0;
|
||||
|
||||
const baseboardMaterial = new BABYLON.PBRMaterial('baseboardMaterial', this.engine.scene);
|
||||
baseboardMaterial.albedoColor = new BABYLON.Color3(0.8, 0.8, 0.8);
|
||||
baseboardMaterial.roughness = 0.7;
|
||||
baseboardMaterial.metallic = 0;
|
||||
|
||||
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.engine.scene);
|
||||
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene);
|
||||
this.skyboxMat.backFaceCulling = false;
|
||||
this.skyboxMat.disableLighting = true;
|
||||
this.skybox.material = this.skyboxMat;
|
||||
this.skybox.infiniteDistance = true;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('env:RoomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(300);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
this.applyRoomLight();
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.0005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
//shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
//shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01;
|
||||
this.registerShadowGenerator(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
this.sunLight = new BABYLON.DirectionalLight('env:SunLight', new BABYLON.Vector3(0.2, -1, -1), this.engine.scene);
|
||||
this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
|
||||
this.sunLight.shadowMinZ = cm(1000);
|
||||
this.sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.sunLight);
|
||||
shadowGeneratorForSunLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForSunLight.bias = 0.00001;
|
||||
shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForSunLight.usePoissonSampling = true;
|
||||
this.registerShadowGenerator(shadowGeneratorForSunLight);
|
||||
}
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
}
|
||||
|
||||
public async load(options: SimpleEnvOptions) {
|
||||
this.loaderResult = await BABYLON.LoadAssetContainerAsync('/client-assets/room/envs/simple/300.glb', this.engine.scene);
|
||||
|
||||
const collisionScalingContainer = new BABYLON.TransformNode('collisionScalingContainer', this.engine.scene);
|
||||
collisionScalingContainer.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
collisionScalingContainer.parent = this.rootNode;
|
||||
const collision = this.loaderResult.meshes.find(m => m.name.includes('__COLLISION__'))!;
|
||||
collision.parent = collisionScalingContainer;
|
||||
|
||||
const lightBlockerScalingContainer = new BABYLON.TransformNode('lightBlockerScalingContainer', this.engine.scene);
|
||||
lightBlockerScalingContainer.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
lightBlockerScalingContainer.parent = this.rootNode;
|
||||
const lightBlocker = this.loaderResult.meshes.find(m => m.name.includes('__LIGHT_BLOCKER__'))!;
|
||||
lightBlocker.parent = lightBlockerScalingContainer;
|
||||
lightBlocker.rotationQuaternion = null;
|
||||
lightBlocker.rotation.y = Math.PI;
|
||||
|
||||
const originalFloorRoot = this.loaderResult.transformNodes.find(t => t.name.includes('__FLOOR__'))!;
|
||||
originalFloorRoot.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
originalFloorRoot.parent = this.rootNode;
|
||||
for (const child of originalFloorRoot.getChildMeshes()) {
|
||||
if (child.material.name.includes('__FLOOR__')) {
|
||||
child.material = this.floorMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
const originalCeilingRoot = this.loaderResult.transformNodes.find(t => t.name.includes('__CEILING__'))!;
|
||||
originalCeilingRoot.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
originalCeilingRoot.position.y = cm(250);
|
||||
originalCeilingRoot.parent = this.rootNode;
|
||||
for (const child of originalCeilingRoot.getChildMeshes()) {
|
||||
if (child.material.name.includes('__CEILING__')) {
|
||||
child.material = this.ceilingMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
public applyOptions(options: SimpleEnvOptions) {
|
||||
// clean up
|
||||
for (const type of ['zPositive', 'zNegative', 'xPositive', 'xNegative'] as const) {
|
||||
const wallRoot = this.wallScalingContainers[type];
|
||||
for (const mesh of wallRoot.getChildMeshes()) {
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.removeShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
for (const type of ['zp_xp', 'zp_xn', 'zn_xp', 'zn_xn'] as const) {
|
||||
const pillarRoot = this.pillarScalingContainers[type];
|
||||
for (const mesh of pillarRoot.getChildMeshes()) {
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.removeShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 返り値をpromiseにしてちゃんとテクスチャが読み終わってからresolveする
|
||||
|
||||
for (const type of ['zPositive', 'zNegative', 'xPositive', 'xNegative'] as const) {
|
||||
const wallRoot = this.wallScalingContainers[type];
|
||||
const wallOptions = options.walls[type];
|
||||
|
||||
const originalRoot =
|
||||
type === 'zPositive' ?
|
||||
options.window === 'kosidakamado' ? this.loaderResult!.transformNodes.find(t => t.name.includes('__WALL_KOSIDAKAMADO__'))! :
|
||||
options.window === 'demado' ? this.loaderResult!.transformNodes.find(t => t.name.includes('__WALL_DEMADO__'))! :
|
||||
this.loaderResult!.transformNodes.find(t => t.name.includes('__WALL__'))!
|
||||
: type === 'zNegative'
|
||||
? this.loaderResult!.transformNodes.find(t => t.name.includes('__WALL_DOOR__'))!
|
||||
: this.loaderResult!.transformNodes.find(t => t.name.includes('__WALL__'))!;
|
||||
|
||||
for (const child of treeClone(originalRoot).getChildren()) {
|
||||
child.parent = wallRoot;
|
||||
}
|
||||
|
||||
for (const child of wallRoot.getChildMeshes()) {
|
||||
if (child.material.name.includes('__WALL__')) {
|
||||
child.material = this.wallMaterials[type];
|
||||
} else if (child.material.name.includes('__BEAM__')) {
|
||||
child.material = this.wallBeamMaterials[type];
|
||||
}
|
||||
}
|
||||
|
||||
for (const mesh of wallRoot.getChildMeshes()) {
|
||||
if (mesh.name.includes('__BEAM__')) {
|
||||
mesh.setEnabled(wallOptions.withBeam);
|
||||
} else if (mesh.name.includes('__BASEBOARD__')) {
|
||||
mesh.setEnabled(wallOptions.withBaseboard);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const targetMaterial = this.wallMaterials[type];
|
||||
|
||||
targetMaterial.unfreeze();
|
||||
targetMaterial.albedoColor = new BABYLON.Color3(...wallOptions.color);
|
||||
|
||||
const texPath = wallOptions.material === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: wallOptions.material === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
tex.wrapU = BABYLON.Texture.WRAP_ADDRESSMODE;
|
||||
tex.wrapV = BABYLON.Texture.WRAP_ADDRESSMODE;
|
||||
targetMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
targetMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
targetMaterial.freeze();
|
||||
}
|
||||
|
||||
{
|
||||
const targetMaterial = this.wallBeamMaterials[type];
|
||||
|
||||
targetMaterial.unfreeze();
|
||||
targetMaterial.albedoColor = new BABYLON.Color3(...wallOptions.beamColor);
|
||||
|
||||
const texPath = wallOptions.beamMaterial === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: wallOptions.beamMaterial === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
targetMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
targetMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
targetMaterial.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
for (const type of ['zp_xp', 'zp_xn', 'zn_xp', 'zn_xn'] as const) {
|
||||
const pillarRoot = this.pillarScalingContainers[type];
|
||||
const pillarOptions = options.pillars[type];
|
||||
|
||||
let isEnabled = pillarOptions.show;
|
||||
if (!isEnabled) {
|
||||
// 梁同士が直交することは許さない(z-fightingが発生する)ので柱を強制追加
|
||||
if (type === 'zp_xp') {
|
||||
isEnabled = options.walls.zPositive.withBeam && options.walls.xPositive.withBeam;
|
||||
} else if (type === 'zp_xn') {
|
||||
isEnabled = options.walls.zPositive.withBeam && options.walls.xNegative.withBeam;
|
||||
} else if (type === 'zn_xp') {
|
||||
isEnabled = options.walls.zNegative.withBeam && options.walls.xPositive.withBeam;
|
||||
} else if (type === 'zn_xn') {
|
||||
isEnabled = options.walls.zNegative.withBeam && options.walls.xNegative.withBeam;
|
||||
}
|
||||
}
|
||||
if (!isEnabled) continue;
|
||||
|
||||
const originalRoot = this.loaderResult!.transformNodes.find(t => t.name.includes('__PILLAR__'))!;
|
||||
for (const child of treeClone(originalRoot).getChildren()) {
|
||||
child.parent = pillarRoot;
|
||||
}
|
||||
|
||||
for (const child of pillarRoot.getChildMeshes()) {
|
||||
if (child.material.name.includes('__PILLAR__')) {
|
||||
child.material = this.pillarMaterials[type];
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const targetMaterial = this.pillarMaterials[type];
|
||||
|
||||
targetMaterial.unfreeze();
|
||||
targetMaterial.albedoColor = new BABYLON.Color3(...pillarOptions.color);
|
||||
|
||||
const texPath = pillarOptions.material === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: pillarOptions.material === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
targetMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
targetMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
targetMaterial.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
this.ceilingMaterial.unfreeze();
|
||||
this.ceilingMaterial.albedoColor = new BABYLON.Color3(...options.ceiling.color);
|
||||
|
||||
const texPath = options.ceiling.material === 'wood' ? '/client-assets/room/textures/ceiling-wood.png'
|
||||
: options.ceiling.material === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
this.ceilingMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
this.ceilingMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
this.ceilingMaterial.freeze();
|
||||
}
|
||||
|
||||
{
|
||||
this.floorMaterial.unfreeze();
|
||||
this.floorMaterial.albedoColor = new BABYLON.Color3(...options.flooring.color);
|
||||
|
||||
const texPath = options.flooring.material === 'wood' ? '/client-assets/room/textures/flooring-wood.png'
|
||||
: options.flooring.material === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
this.floorMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
this.floorMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
this.floorMaterial.freeze();
|
||||
}
|
||||
|
||||
for (const mesh of this.rootNode.getChildMeshes()) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
//if (mesh.material !== this.floorMaterial) { // 床は他の何にも影を落とさないことが確定している
|
||||
this.addShadowCaster(mesh);
|
||||
//}
|
||||
}
|
||||
this.registerMeshes(this.rootNode.getChildMeshes());
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
if (this.skyboxMat == null) return;
|
||||
|
||||
if (time === 0) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (time === 1) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
|
||||
if (this.sunLight != null) {
|
||||
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public applyRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.light.color);
|
||||
this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0);
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025 + (0.575 * this.engine.roomState.light.brightness * (this.isRoomLightOn ? 1 : 0));
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = this.isRoomLightOn ? new BABYLON.Color3(0.5, 0.5, 0.5) : new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const m of this.rootNode.getChildMeshes()) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const m of Object.values(this.wallMaterials ?? {})) {
|
||||
m.dispose();
|
||||
}
|
||||
for (const m of Object.values(this.wallBeamMaterials ?? {})) {
|
||||
m.dispose();
|
||||
}
|
||||
for (const m of Object.values(this.pillarMaterials ?? {})) {
|
||||
m.dispose();
|
||||
}
|
||||
this.skybox?.dispose();
|
||||
this.skyboxMat?.dispose();
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
this.sunLight?.dispose();
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user