Merge pull request #82 from medievalshell/dev

feat: rare values + fortune wheel protocol + prize editor + feat: soundboard packets
This commit is contained in:
DuckieTM
2026-05-28 13:50:27 +02:00
committed by GitHub
38 changed files with 587 additions and 17 deletions
@@ -1,7 +1,7 @@
import { IAssetManager, IAvatarFigureContainer, IAvatarImageListener } from '@nitrots/api';
import { GetConfiguration } from '@nitrots/configuration';
import { AvatarRenderLibraryEvent, GetEventDispatcher, NitroEvent, NitroEventType } from '@nitrots/events';
import { parseConfigJsonFromResponse } from '@nitrots/utils';
import { loadGamedata } from '@nitrots/utils';
import { AvatarAssetDownloadLibrary } from './AvatarAssetDownloadLibrary';
import { AvatarStructure } from './AvatarStructure';
@@ -41,28 +41,19 @@ export class AvatarAssetDownloadManager
if(!url || !url.length) throw new Error('Missing "avatar.figuremap.url" in config — add the figure map URL to your renderer-config.json');
let response: Response;
try
{
response = await fetch(url);
}
catch(fetchErr)
{
throw new Error(`Could not fetch figure map from "${ url }" — check "avatar.figuremap.url" in renderer-config.json (${ fetchErr.message })`);
}
if(response.status !== 200) throw new Error(`Failed to load figure map from "${ url }" — server returned HTTP ${ response.status }. Check "avatar.figuremap.url" in renderer-config.json`);
let responseData: any;
try
{
responseData = await parseConfigJsonFromResponse(response, url);
// Split-aware loader (tier manifest -> per-tier files -> merged).
// The figuremap URL is a directory manifest, not a single JSON, so a
// raw fetch yields { tiers: [...] } with no `libraries` and avatars
// silently render empty. loadGamedata handles both split + single.
responseData = await loadGamedata(url);
}
catch(parseErr)
catch(fetchErr)
{
throw new Error(`Invalid figure map "${ url }" — JSON/JSON5 parse failed. Check "avatar.figuremap.url" in renderer-config.json (${ parseErr.message })`);
throw new Error(`Could not load figure map from "${ url }" — check "avatar.figuremap.url" in renderer-config.json (${ fetchErr.message })`);
}
this.processFigureMap(responseData.libraries);
@@ -8,6 +8,10 @@ import { HanditemBlockStateMessageEvent } from './messages';
import { TranslationLanguagesEvent, TranslationLanguagesRequestComposer, TranslationResultEvent, TranslationTextRequestComposer } from './messages';
import { YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomSettingsComposer, YouTubeRoomSettingsEvent, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from './messages';
import { HousekeepingActionLogEvent, HousekeepingActionResultEvent, HousekeepingBanUserComposer, HousekeepingDashboardEvent, HousekeepingDeleteRoomComposer, HousekeepingFindRoomByIdComposer, HousekeepingFindUserByIdComposer, HousekeepingFindUserByNameComposer, HousekeepingForceDisconnectUserComposer, HousekeepingGetDashboardComposer, HousekeepingGiveCreditsComposer, HousekeepingGiveCurrencyComposer, HousekeepingGrantItemComposer, HousekeepingKickAllFromRoomComposer, HousekeepingKickUserComposer, HousekeepingListActionLogComposer, HousekeepingMuteRoomComposer, HousekeepingMuteUserComposer, HousekeepingResetUserPasswordComposer, HousekeepingRoomDetailEvent, HousekeepingRoomListEvent, HousekeepingRoomStateComposer, HousekeepingSearchRoomsComposer, HousekeepingSendHotelAlertComposer, HousekeepingSetHcSubscriptionComposer, HousekeepingSetUserRankComposer, HousekeepingTradeLockUserComposer, HousekeepingTransferRoomOwnershipComposer, HousekeepingUnbanUserComposer, HousekeepingUserDetailEvent } from './messages';
import { RareValuesEvent, RequestRareValuesComposer } from './messages';
import { WheelBuySpinComposer, WheelDataEvent, WheelOpenComposer, WheelRecentWinsEvent, WheelResultEvent, WheelSpinComposer } from './messages';
import { WheelAdminGetPrizesComposer, WheelAdminPrizesEvent, WheelAdminSavePrizesComposer } from './messages';
import { SoundboardPlayEvent, SoundboardSettingsEvent, SoundboardPlayComposer, SoundboardSetEnabledComposer } from './messages';
export class NitroMessages implements IMessageConfiguration
{
private _events: Map<number, Function>;
@@ -514,6 +518,15 @@ export class NitroMessages implements IMessageConfiguration
this._events.set(IncomingHeader.HOUSEKEEPING_ROOM_LIST, HousekeepingRoomListEvent);
this._events.set(IncomingHeader.HOUSEKEEPING_DASHBOARD, HousekeepingDashboardEvent);
this._events.set(IncomingHeader.HOUSEKEEPING_ACTION_LOG, HousekeepingActionLogEvent);
// Custom features
this._events.set(IncomingHeader.RARE_VALUES, RareValuesEvent);
this._events.set(IncomingHeader.WHEEL_DATA, WheelDataEvent);
this._events.set(IncomingHeader.WHEEL_RESULT, WheelResultEvent);
this._events.set(IncomingHeader.WHEEL_RECENT_WINS, WheelRecentWinsEvent);
this._events.set(IncomingHeader.WHEEL_ADMIN_PRIZES, WheelAdminPrizesEvent);
this._events.set(IncomingHeader.SOUNDBOARD_SETTINGS, SoundboardSettingsEvent);
this._events.set(IncomingHeader.SOUNDBOARD_PLAY, SoundboardPlayEvent);
this._events.set(IncomingHeader.WIRED_REWARD, WiredRewardResultMessageEvent);
this._events.set(IncomingHeader.WIRED_SAVE, WiredSaveSuccessEvent);
this._events.set(IncomingHeader.WIRED_ERROR, WiredValidationErrorEvent);
@@ -1290,6 +1303,16 @@ export class NitroMessages implements IMessageConfiguration
this._composers.set(OutgoingHeader.HOUSEKEEPING_SEND_HOTEL_ALERT, HousekeepingSendHotelAlertComposer);
this._composers.set(OutgoingHeader.HOUSEKEEPING_GET_DASHBOARD, HousekeepingGetDashboardComposer);
this._composers.set(OutgoingHeader.HOUSEKEEPING_LIST_ACTION_LOG, HousekeepingListActionLogComposer);
// Custom features
this._composers.set(OutgoingHeader.REQUEST_RARE_VALUES, RequestRareValuesComposer);
this._composers.set(OutgoingHeader.WHEEL_OPEN, WheelOpenComposer);
this._composers.set(OutgoingHeader.WHEEL_SPIN, WheelSpinComposer);
this._composers.set(OutgoingHeader.WHEEL_BUY_SPIN, WheelBuySpinComposer);
this._composers.set(OutgoingHeader.WHEEL_ADMIN_GET_PRIZES, WheelAdminGetPrizesComposer);
this._composers.set(OutgoingHeader.WHEEL_ADMIN_SAVE_PRIZES, WheelAdminSavePrizesComposer);
this._composers.set(OutgoingHeader.SOUNDBOARD_PLAY, SoundboardPlayComposer);
this._composers.set(OutgoingHeader.SOUNDBOARD_SET_ENABLED, SoundboardSetEnabledComposer);
}
public get events(): Map<number, Function>
@@ -511,4 +511,13 @@ export class IncomingHeader
public static HOUSEKEEPING_ROOM_LIST = 9203;
public static HOUSEKEEPING_DASHBOARD = 9204;
public static HOUSEKEEPING_ACTION_LOG = 9205;
// Custom features — IDs 9400+ reserved
public static RARE_VALUES = 9400;
public static WHEEL_DATA = 9401;
public static WHEEL_RESULT = 9402;
public static WHEEL_RECENT_WINS = 9403;
public static WHEEL_ADMIN_PRIZES = 9404;
public static SOUNDBOARD_SETTINGS = 9405;
public static SOUNDBOARD_PLAY = 9406;
}
@@ -25,6 +25,9 @@ export * from './groupforums';
export * from './handshake';
export * from './help';
export * from './housekeeping';
export * from './rarevalues';
export * from './soundboard';
export * from './wheel';
export * from './inventory';
export * from './inventory/achievements';
export * from './inventory/avatareffect';
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { RareValuesParser } from '../../parser';
export class RareValuesEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, RareValuesParser);
}
public getParser(): RareValuesParser
{
return this.parser as RareValuesParser;
}
}
@@ -0,0 +1 @@
export * from './RareValuesEvent';
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { SoundboardPlayParser } from '../../parser';
export class SoundboardPlayEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, SoundboardPlayParser);
}
public getParser(): SoundboardPlayParser
{
return this.parser as SoundboardPlayParser;
}
}
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { SoundboardSettingsParser } from '../../parser';
export class SoundboardSettingsEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, SoundboardSettingsParser);
}
public getParser(): SoundboardSettingsParser
{
return this.parser as SoundboardSettingsParser;
}
}
@@ -0,0 +1,2 @@
export * from './SoundboardPlayEvent';
export * from './SoundboardSettingsEvent';
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { WheelAdminPrizesParser } from '../../parser';
export class WheelAdminPrizesEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, WheelAdminPrizesParser);
}
public getParser(): WheelAdminPrizesParser
{
return this.parser as WheelAdminPrizesParser;
}
}
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { WheelDataParser } from '../../parser';
export class WheelDataEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, WheelDataParser);
}
public getParser(): WheelDataParser
{
return this.parser as WheelDataParser;
}
}
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { WheelRecentWinsParser } from '../../parser';
export class WheelRecentWinsEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, WheelRecentWinsParser);
}
public getParser(): WheelRecentWinsParser
{
return this.parser as WheelRecentWinsParser;
}
}
@@ -0,0 +1,16 @@
import { IMessageEvent } from '@nitrots/api';
import { MessageEvent } from '@nitrots/events';
import { WheelResultParser } from '../../parser';
export class WheelResultEvent extends MessageEvent implements IMessageEvent
{
constructor(callBack: Function)
{
super(callBack, WheelResultParser);
}
public getParser(): WheelResultParser
{
return this.parser as WheelResultParser;
}
}
@@ -0,0 +1,4 @@
export * from './WheelDataEvent';
export * from './WheelResultEvent';
export * from './WheelRecentWinsEvent';
export * from './WheelAdminPrizesEvent';
@@ -548,4 +548,14 @@ export class OutgoingHeader
public static HOUSEKEEPING_SEND_HOTEL_ALERT = 9121;
public static HOUSEKEEPING_GET_DASHBOARD = 9122;
public static HOUSEKEEPING_LIST_ACTION_LOG = 9123;
// Custom features — IDs 9300+ reserved
public static REQUEST_RARE_VALUES = 9300;
public static WHEEL_OPEN = 9301;
public static WHEEL_SPIN = 9302;
public static WHEEL_BUY_SPIN = 9303;
public static WHEEL_ADMIN_GET_PRIZES = 9304;
public static WHEEL_ADMIN_SAVE_PRIZES = 9305;
public static SOUNDBOARD_PLAY = 9306;
public static SOUNDBOARD_SET_ENABLED = 9307;
}
@@ -22,6 +22,9 @@ export * from './groupforums';
export * from './handshake';
export * from './help';
export * from './housekeeping';
export * from './rarevalues';
export * from './soundboard';
export * from './wheel';
export * from './inventory';
export * from './inventory/avatareffect';
export * from './inventory/badges';
@@ -0,0 +1,7 @@
import { IMessageComposer } from '@nitrots/api';
export class RequestRareValuesComposer implements IMessageComposer<[]>
{
public getMessageArray(): [] { return []; }
public dispose(): void { return; }
}
@@ -0,0 +1 @@
export * from './RequestRareValuesComposer';
@@ -0,0 +1,14 @@
import { IMessageComposer } from '@nitrots/api';
export class SoundboardPlayComposer implements IMessageComposer<[ number ]>
{
private _data: [ number ];
constructor(soundId: number)
{
this._data = [ soundId ];
}
public getMessageArray(): [ number ] { return this._data; }
public dispose(): void { return; }
}
@@ -0,0 +1,14 @@
import { IMessageComposer } from '@nitrots/api';
export class SoundboardSetEnabledComposer implements IMessageComposer<[ number ]>
{
private _data: [ number ];
constructor(enabled: boolean)
{
this._data = [ enabled ? 1 : 0 ];
}
public getMessageArray(): [ number ] { return this._data; }
public dispose(): void { return; }
}
@@ -0,0 +1,2 @@
export * from './SoundboardPlayComposer';
export * from './SoundboardSetEnabledComposer';
@@ -0,0 +1,7 @@
import { IMessageComposer } from '@nitrots/api';
export class WheelAdminGetPrizesComposer implements IMessageComposer<[]>
{
public getMessageArray(): [] { return []; }
public dispose(): void { return; }
}
@@ -0,0 +1,32 @@
import { IMessageComposer } from '@nitrots/api';
export interface IWheelAdminPrizeEdit
{
id: number;
type: string;
value: string;
amount: number;
pointsType: number;
weight: number;
label: string;
}
export class WheelAdminSavePrizesComposer implements IMessageComposer<(number | string)[]>
{
private _data: (number | string)[];
constructor(prizes: IWheelAdminPrizeEdit[])
{
const data: (number | string)[] = [ prizes.length ];
for(const prize of prizes)
{
data.push(prize.id, prize.type, prize.value, prize.amount, prize.pointsType, prize.weight, prize.label);
}
this._data = data;
}
public getMessageArray(): (number | string)[] { return this._data; }
public dispose(): void { return; }
}
@@ -0,0 +1,7 @@
import { IMessageComposer } from '@nitrots/api';
export class WheelBuySpinComposer implements IMessageComposer<[]>
{
public getMessageArray(): [] { return []; }
public dispose(): void { return; }
}
@@ -0,0 +1,7 @@
import { IMessageComposer } from '@nitrots/api';
export class WheelOpenComposer implements IMessageComposer<[]>
{
public getMessageArray(): [] { return []; }
public dispose(): void { return; }
}
@@ -0,0 +1,7 @@
import { IMessageComposer } from '@nitrots/api';
export class WheelSpinComposer implements IMessageComposer<[]>
{
public getMessageArray(): [] { return []; }
public dispose(): void { return; }
}
@@ -0,0 +1,5 @@
export * from './WheelOpenComposer';
export * from './WheelSpinComposer';
export * from './WheelBuySpinComposer';
export * from './WheelAdminGetPrizesComposer';
export * from './WheelAdminSavePrizesComposer';
@@ -25,6 +25,9 @@ export * from './groupforums';
export * from './handshake';
export * from './help';
export * from './housekeeping';
export * from './rarevalues';
export * from './soundboard';
export * from './wheel';
export * from './inventory';
export * from './inventory/achievements';
export * from './inventory/avatareffect';
@@ -0,0 +1,41 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export interface IRareValue
{
credits: number;
points: number;
pointsType: number;
}
export class RareValuesParser implements IMessageParser
{
private _values: Map<number, IRareValue> = new Map();
public flush(): boolean
{
this._values = new Map();
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
const count = wrapper.readInt();
for(let i = 0; i < count; i++)
{
const spriteId = wrapper.readInt();
const credits = wrapper.readInt();
const points = wrapper.readInt();
const pointsType = wrapper.readInt();
this._values.set(spriteId, { credits, points, pointsType });
}
return true;
}
public get values(): Map<number, IRareValue> { return this._values; }
}
@@ -0,0 +1 @@
export * from './RareValuesParser';
@@ -0,0 +1,32 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export class SoundboardPlayParser implements IMessageParser
{
private _soundId: number = 0;
private _url: string = '';
private _username: string = '';
public flush(): boolean
{
this._soundId = 0;
this._url = '';
this._username = '';
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
this._soundId = wrapper.readInt();
this._url = wrapper.readString();
this._username = wrapper.readString();
return true;
}
public get soundId(): number { return this._soundId; }
public get url(): string { return this._url; }
public get username(): string { return this._username; }
}
@@ -0,0 +1,46 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export interface ISoundboardSound
{
id: number;
name: string;
url: string;
}
export class SoundboardSettingsParser implements IMessageParser
{
private _enabled: boolean = false;
private _sounds: ISoundboardSound[] = [];
public flush(): boolean
{
this._enabled = false;
this._sounds = [];
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
this._enabled = wrapper.readBoolean();
const count = wrapper.readInt();
this._sounds = [];
for(let i = 0; i < count; i++)
{
this._sounds.push({
id: wrapper.readInt(),
name: wrapper.readString(),
url: wrapper.readString()
});
}
return true;
}
public get enabled(): boolean { return this._enabled; }
public get sounds(): ISoundboardSound[] { return this._sounds; }
}
@@ -0,0 +1,2 @@
export * from './SoundboardPlayParser';
export * from './SoundboardSettingsParser';
@@ -0,0 +1,49 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export interface IWheelAdminPrize
{
id: number;
type: string;
value: string;
amount: number;
pointsType: number;
weight: number;
label: string;
}
export class WheelAdminPrizesParser implements IMessageParser
{
private _prizes: IWheelAdminPrize[] = [];
public flush(): boolean
{
this._prizes = [];
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
const count = wrapper.readInt();
this._prizes = [];
for(let i = 0; i < count; i++)
{
this._prizes.push({
id: wrapper.readInt(),
type: wrapper.readString(),
value: wrapper.readString(),
amount: wrapper.readInt(),
pointsType: wrapper.readInt(),
weight: wrapper.readInt(),
label: wrapper.readString()
});
}
return true;
}
public get prizes(): IWheelAdminPrize[] { return this._prizes; }
}
@@ -0,0 +1,66 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export interface IWheelPrize
{
id: number;
type: string; // item | badge | credits | points | spin | nothing
spriteId: number; // item only (furni icon), else 0
badgeCode: string; // badge only, else ''
amount: number;
pointsType: number;
label: string;
}
export class WheelDataParser implements IMessageParser
{
private _freeSpins: number = 0;
private _extraSpins: number = 0;
private _spinCost: number = 0;
private _spinCostType: number = 0;
private _prizes: IWheelPrize[] = [];
public flush(): boolean
{
this._freeSpins = 0;
this._extraSpins = 0;
this._spinCost = 0;
this._spinCostType = 0;
this._prizes = [];
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
this._freeSpins = wrapper.readInt();
this._extraSpins = wrapper.readInt();
this._spinCost = wrapper.readInt();
this._spinCostType = wrapper.readInt();
const count = wrapper.readInt();
this._prizes = [];
for(let i = 0; i < count; i++)
{
this._prizes.push({
id: wrapper.readInt(),
type: wrapper.readString(),
spriteId: wrapper.readInt(),
badgeCode: wrapper.readString(),
amount: wrapper.readInt(),
pointsType: wrapper.readInt(),
label: wrapper.readString()
});
}
return true;
}
public get freeSpins(): number { return this._freeSpins; }
public get extraSpins(): number { return this._extraSpins; }
public get spinCost(): number { return this._spinCost; }
public get spinCostType(): number { return this._spinCostType; }
public get prizes(): IWheelPrize[] { return this._prizes; }
}
@@ -0,0 +1,41 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export interface IWheelRecentWin
{
username: string;
look: string;
prizeLabel: string;
}
export class WheelRecentWinsParser implements IMessageParser
{
private _wins: IWheelRecentWin[] = [];
public flush(): boolean
{
this._wins = [];
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
const count = wrapper.readInt();
this._wins = [];
for(let i = 0; i < count; i++)
{
this._wins.push({
username: wrapper.readString(),
look: wrapper.readString(),
prizeLabel: wrapper.readString()
});
}
return true;
}
public get wins(): IWheelRecentWin[] { return this._wins; }
}
@@ -0,0 +1,24 @@
import { IMessageDataWrapper, IMessageParser } from '@nitrots/api';
export class WheelResultParser implements IMessageParser
{
private _prizeId: number = 0;
public flush(): boolean
{
this._prizeId = 0;
return true;
}
public parse(wrapper: IMessageDataWrapper): boolean
{
if(!wrapper) return false;
this._prizeId = wrapper.readInt();
return true;
}
public get prizeId(): number { return this._prizeId; }
}
@@ -0,0 +1,4 @@
export * from './WheelDataParser';
export * from './WheelResultParser';
export * from './WheelRecentWinsParser';
export * from './WheelAdminPrizesParser';