This commit is contained in:
Bill 2021-09-09 03:14:44 -04:00
parent a0400b7a88
commit 2961fc75d4
61 changed files with 1001 additions and 84 deletions

1
.env
View File

@ -1 +0,0 @@
CONFIG_URL=http://client.nitrots.co:3000/renderer-config.json

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# Nitro Imager
This tool serves as a server-side habbo-imager using the same avatar generator from nitro-renderer. It will download & cache in memory ``.nitro`` assets. Rendered figures will also save to a local folder to prevent re-renders. You will use the same process as your nitro-client to update assets for the imager.
## Configuration
**Make sure you run ``npm i`` before first use.**
You must configure your urls in `config.json`
Your figuredata, figuremap, effectmap, & HabboAvatarActions can safely point to a remote URL without worrying about performance.
You should set all download urls to local absolute paths on your system, this will allow for faster downloading of figures. However, you may point to remote urls as well.
You must also set an absolute path to a location where rendered figures can save to. This can be a private folder that is not accessible from the web.
## URL paramaters
Their are a few different options you may pass as URL parameters to generate figures with different actions. All parameters are optional.
| key | default | description |
| ------ | ------ | ------ |
| figure | null | The figure string to be rendered |
| action | null | The actions to render, see actions below |
| gesture | std | The gesture to render, see gestures below |
| direction | 2 | The direction to render, from 0-7 |
| head_direction | 2 | The head direction to render, from 0-7 |
| headonly | 0 | A value of ``0`` or ``1`` |
| dance | 0 | A dance id of 0-4 to render |
| effect | 0 | An effect id to render |
| size | n | The size to render, see sizes below |
| frame_num | 0 | The frame number to render |
| img_format | png | A value of ``png`` or ``gif``. Gif will render all frames of the figure |
## Actions
You may render multiple actions with a comma separater
Example: ``&action=wlk,wav,drk=1``
##### Posture
| key | description |
| ------ | ------ |
| std | Renders the standing posture |
| wlk,mv | Renders the walking posture |
| sit | Renders the sitting posture |
| lay | Renders the laying posture |
##### Expression
| key | description |
| ------ | ------ |
| wav,wave | Renders the waving expression |
| blow | Renders the kissing expression |
| laugh | Renders the laughing expression |
| respect | Renders the respect expression |
##### Carry / Drink
To hold a certain drink, use an equal separator with the hand item id. You can only render one of these options at a time
| key | description |
| ------ | ------ |
| crr,cri | Renders the carry action |
| drk,usei | Renders the drink action |
## Gestures
| key | description |
| ------ | ------ |
| std | Renders the standard gesture |
| agr | Renders the aggravated gesture |
| sad | Renders the sad gesture |
| sml | Renders the smile gesture |
| srp | Renders the surprised gesture |
## Sizes
| key | description |
| ------ | ------ |
| s | Renders the small size (0.5) |
| n | Renders the normal size (1) |
| l | Renders the large size (2) |
## Known Issues
* GIFs are only able to render 1 bit alpha channels, therefore most effects will not correctly render due to using many different alpha values.
* The rendered canvas size may not match habbos imager exactly, we will hopefully have this addressed soon.

38
config.json Normal file
View File

@ -0,0 +1,38 @@
{
"api.host": "localhost",
"api.port": 3030,
"asset.url": "ABSOLUTE_ASSET_URL_WITHOUT_SLASH",
"gamedata.url": "${asset.url}/gamedata",
"avatar.save.path": "ABSOLUTE_PATH/saved-figures",
"avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json",
"avatar.figuredata.url": "${gamedata.url}/FigureData.json",
"avatar.figuremap.url": "${gamedata.url}/FigureMap.json",
"avatar.effectmap.url": "${gamedata.url}/EffectMap.json",
"avatar.asset.url": "${asset.url}/bundled/figure/%libname%.nitro",
"avatar.asset.effect.url": "${asset.url}/bundled/effect/%libname%.nitro",
"avatar.mandatory.libraries": [
"bd:1",
"li:0"
],
"avatar.mandatory.effect.libraries": [
"dance.1",
"dance.2",
"dance.3",
"dance.4"
],
"avatar.default.figuredata": {"palettes":[{"id":1,"colors":[{"id":99999,"index":1001,"club":0,"selectable":false,"hexCode":"DDDDDD"},{"id":99998,"index":1001,"club":0,"selectable":false,"hexCode":"FAFAFA"}]},{"id":3,"colors":[{"id":10001,"index":1001,"club":0,"selectable":false,"hexCode":"EEEEEE"},{"id":10002,"index":1002,"club":0,"selectable":false,"hexCode":"FA3831"},{"id":10003,"index":1003,"club":0,"selectable":false,"hexCode":"FD92A0"},{"id":10004,"index":1004,"club":0,"selectable":false,"hexCode":"2AC7D2"},{"id":10005,"index":1005,"club":0,"selectable":false,"hexCode":"35332C"},{"id":10006,"index":1006,"club":0,"selectable":false,"hexCode":"EFFF92"},{"id":10007,"index":1007,"club":0,"selectable":false,"hexCode":"C6FF98"},{"id":10008,"index":1008,"club":0,"selectable":false,"hexCode":"FF925A"},{"id":10009,"index":1009,"club":0,"selectable":false,"hexCode":"9D597E"},{"id":10010,"index":1010,"club":0,"selectable":false,"hexCode":"B6F3FF"},{"id":10011,"index":1011,"club":0,"selectable":false,"hexCode":"6DFF33"},{"id":10012,"index":1012,"club":0,"selectable":false,"hexCode":"3378C9"},{"id":10013,"index":1013,"club":0,"selectable":false,"hexCode":"FFB631"},{"id":10014,"index":1014,"club":0,"selectable":false,"hexCode":"DFA1E9"},{"id":10015,"index":1015,"club":0,"selectable":false,"hexCode":"F9FB32"},{"id":10016,"index":1016,"club":0,"selectable":false,"hexCode":"CAAF8F"},{"id":10017,"index":1017,"club":0,"selectable":false,"hexCode":"C5C6C5"},{"id":10018,"index":1018,"club":0,"selectable":false,"hexCode":"47623D"},{"id":10019,"index":1019,"club":0,"selectable":false,"hexCode":"8A8361"},{"id":10020,"index":1020,"club":0,"selectable":false,"hexCode":"FF8C33"},{"id":10021,"index":1021,"club":0,"selectable":false,"hexCode":"54C627"},{"id":10022,"index":1022,"club":0,"selectable":false,"hexCode":"1E6C99"},{"id":10023,"index":1023,"club":0,"selectable":false,"hexCode":"984F88"},{"id":10024,"index":1024,"club":0,"selectable":false,"hexCode":"77C8FF"},{"id":10025,"index":1025,"club":0,"selectable":false,"hexCode":"FFC08E"},{"id":10026,"index":1026,"club":0,"selectable":false,"hexCode":"3C4B87"},{"id":10027,"index":1027,"club":0,"selectable":false,"hexCode":"7C2C47"},{"id":10028,"index":1028,"club":0,"selectable":false,"hexCode":"D7FFE3"},{"id":10029,"index":1029,"club":0,"selectable":false,"hexCode":"8F3F1C"},{"id":10030,"index":1030,"club":0,"selectable":false,"hexCode":"FF6393"},{"id":10031,"index":1031,"club":0,"selectable":false,"hexCode":"1F9B79"},{"id":10032,"index":1032,"club":0,"selectable":false,"hexCode":"FDFF33"}]}],"setTypes":[{"type":"hd","paletteId":1,"mandatory_f_0":true,"mandatory_f_1":true,"mandatory_m_0":true,"mandatory_m_1":true,"sets":[{"id":99999,"gender":"U","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":1,"type":"bd","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"hd","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"lh","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"rh","colorable":true,"index":0,"colorindex":1}]}]},{"type":"bds","paletteId":1,"mandatory_f_0":false,"mandatory_f_1":false,"mandatory_m_0":false,"mandatory_m_1":false,"sets":[{"id":10001,"gender":"U","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10001,"type":"bds","colorable":true,"index":0,"colorindex":1},{"id":10001,"type":"lhs","colorable":true,"index":0,"colorindex":1},{"id":10001,"type":"rhs","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"bd"},{"partType":"rh"},{"partType":"lh"}]}]},{"type":"ss","paletteId":3,"mandatory_f_0":false,"mandatory_f_1":false,"mandatory_m_0":false,"mandatory_m_1":false,"sets":[{"id":10010,"gender":"F","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10001,"type":"ss","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"ch"},{"partType":"lg"},{"partType":"ca"},{"partType":"wa"},{"partType":"sh"},{"partType":"ls"},{"partType":"rs"},{"partType":"lc"},{"partType":"rc"},{"partType":"cc"},{"partType":"cp"}]},{"id":10011,"gender":"M","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10002,"type":"ss","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"ch"},{"partType":"lg"},{"partType":"ca"},{"partType":"wa"},{"partType":"sh"},{"partType":"ls"},{"partType":"rs"},{"partType":"lc"},{"partType":"rc"},{"partType":"cc"},{"partType":"cp"}]}]}]},
"avatar.default.actions": {
"actions": [
{
"id": "Default",
"state": "std",
"precedence": 1000,
"main": true,
"isDefault": true,
"geometryType": "vertical",
"activePartSet": "figure",
"assetPartDefinition": "std"
}
]
}
}

1
index.ts Normal file
View File

@ -0,0 +1 @@
require('./src/main');

22
package-lock.json generated
View File

@ -81,6 +81,15 @@
"@types/range-parser": "*"
}
},
"@types/gifencoder": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/gifencoder/-/gifencoder-2.0.1.tgz",
"integrity": "sha512-Ls78JLiLPHA1ytIXMWv/7/71a2Cz7BBnjgi9R/LFcIS531PEFYxPPGHNmBBnLekQ7/VpO+n1fgaJ6XD3ZkpApg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@ -464,11 +473,6 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
},
"dynamic-dedupe": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",
@ -620,6 +624,14 @@
"wide-align": "^1.1.0"
}
},
"gifencoder": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/gifencoder/-/gifencoder-2.0.1.tgz",
"integrity": "sha512-x19DcyWY10SkshBpokqFOo/HBht9GB75evRYvaLMbez9p+yB/o+kt0fK9AwW59nFiAMs2UUQsjv1lX/hvu9Ong==",
"requires": {
"canvas": "^2.2.0"
}
},
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",

View File

@ -2,13 +2,13 @@
"name": "nitro-imager",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "index.ts",
"dependencies": {
"bytebuffer": "^5.0.1",
"canvas": "^2.8.0",
"chalk": "^4.1.2",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"gifencoder": "^2.0.1",
"node-fetch": "^2.6.1",
"pako": "^2.0.4"
},
@ -16,6 +16,7 @@
"@types/bytebuffer": "^5.0.42",
"@types/chalk": "^2.2.0",
"@types/express": "^4.17.13",
"@types/gifencoder": "^2.0.1",
"@types/node": "^14.17.12",
"@types/node-fetch": "^2.5.12",
"@types/pako": "^1.0.2",
@ -23,6 +24,8 @@
"typescript": "^4.4.2"
},
"scripts": {
"build": "tsc",
"start": "node ./dist/index.js",
"start:dev": "ts-node-dev --respawn --transpile-only ./src/main.ts"
},
"repository": {

View File

@ -1,6 +1,8 @@
import * as express from 'express';
import { INitroCore, NitroManager } from '../core';
import { AvatarRenderManager, AvatarScaleType, AvatarSetType, IAvatarRenderManager } from './avatar';
import { AvatarRenderManager, IAvatarRenderManager } from './avatar';
import { IApplication } from './IApplication';
import { HttpRouter } from './router/HttpRouter';
export class Application extends NitroManager implements IApplication
{
@ -25,11 +27,7 @@ export class Application extends NitroManager implements IApplication
if(this._avatar) await this._avatar.init();
const image = await this._avatar.createAvatarImage('hd-207-14.lg-3216-1408.cc-3007-86-88.ha-3054-1408-1408.he-3079-64.ea-1402-0.ch-230-72.hr-110-40', AvatarScaleType.LARGE, 'M');
//image.setDirection(AvatarSetType.FULL, 4);
const canvas = await image.getImage(AvatarSetType.FULL, false);
console.log(canvas.toDataURL());
this.setupRouter();
this.logger.log(`Initialized`);
}
@ -46,6 +44,21 @@ export class Application extends NitroManager implements IApplication
return this._core.configuration.getValue<T>(key, value);
}
private setupRouter(): void
{
const router = express();
router.use('/', HttpRouter);
const host = this.getConfiguration<string>('api.host');
const port = this.getConfiguration<number>('api.port');
router.listen(port, host, () =>
{
this.logger.log(`Server Started ${ host }:${ port }`);
});
}
public get core(): INitroCore
{
return this._core;

View File

@ -42,17 +42,19 @@ export class AvatarAssetDownloadLibrary
return false;
}
public async downloadAsset(): Promise<void>
public async downloadAsset(): Promise<boolean>
{
if(!this._assets || (this._state === AvatarAssetDownloadLibrary.LOADING)) return;
if(!this._assets || (this._state === AvatarAssetDownloadLibrary.LOADING)) return false;
if(this.checkIfAssetLoaded()) return;
if(this.checkIfAssetLoaded()) return false;
this._state = AvatarAssetDownloadLibrary.LOADING;
await this._assets.downloadAsset(this._downloadUrl);
if(!await this._assets.downloadAsset(this._downloadUrl)) return false;
this._state = AvatarAssetDownloadLibrary.LOADED;
return true;
}
public get libraryName(): string

View File

@ -1,5 +1,4 @@
import fetch from 'node-fetch';
import { AdvancedMap, IAssetManager } from '../../core';
import { AdvancedMap, FileUtilities, IAssetManager } from '../../core';
import { Application } from '../Application';
import { AvatarAssetDownloadLibrary } from './AvatarAssetDownloadLibrary';
import { AvatarStructure } from './AvatarStructure';
@ -28,8 +27,8 @@ export class AvatarAssetDownloadManager
{
const url = Application.instance.getConfiguration<string>('avatar.figuremap.url');
const data = await fetch(url);
const json = await data.json();
const data = await FileUtilities.readFileAsString(url);
const json = JSON.parse(data);
this.processFigureMap(json.libraries);
@ -149,6 +148,6 @@ export class AvatarAssetDownloadManager
{
if(!library || library.isLoaded) return;
await library.downloadAsset();
if(!await library.downloadAsset()) return;
}
}

View File

@ -1,5 +1,5 @@
import { Canvas, createCanvas } from 'canvas';
import { IGraphicAsset } from '../../core';
import { Canvas } from 'canvas';
import { CanvasUtilities, IGraphicAsset } from '../../core';
import { ActiveActionData, IActionDefinition, IActiveActionData } from './actions';
import { AssetAliasCollection } from './alias';
import { IAnimationLayerData, IAvatarDataContainer, ISpriteDataContainer } from './animation';
@ -168,7 +168,7 @@ export class AvatarImage implements IAvatarImage
public getCanvasOffsets(): number[]
{
return this._canvasOffsets;
return (this._canvasOffsets || [ 0, 0, 0 ]);
}
public getLayerData(k: ISpriteDataContainer): IAnimationLayerData
@ -186,6 +186,26 @@ export class AvatarImage implements IAvatarImage
this._frameCounter = 0;
}
public getTotalFrameCount(): number
{
const actions = this._sortedActions;
let frames = this._animationFrameCount;
for(const action of actions)
{
const animation = this._structure.animationManager.getAnimation(((action.definition.state + '.') + action.actionParameter));
if(!animation) continue;
const frameCount = animation.frameCount(action.overridingAction);
frames = Math.max(frames, frameCount);
}
return frames;
}
private getFullImageCacheKey(): string
{
if(((this._sortedActions.length == 1) && (this._mainDirection == this._headDirection)))
@ -257,7 +277,7 @@ export class AvatarImage implements IAvatarImage
}
}
public async getImage(setType: string, hightlight: boolean, scale: number = 1): Promise<Canvas>
public async getImage(setType: string, bgColor: number = 0, hightlight: boolean = false, scale: number = 1): Promise<Canvas>
{
if(!this._mainAction) return null;
@ -269,9 +289,16 @@ export class AvatarImage implements IAvatarImage
const bodyParts = this.getBodyParts(setType, this._mainAction.definition.geometryType, this._mainDirection);
const canvas = createCanvas(avatarCanvas.width, avatarCanvas.height);
const canvas = CanvasUtilities.createNitroCanvas(avatarCanvas.width, avatarCanvas.height);
const ctx = canvas.getContext('2d');
if(bgColor > 0)
{
ctx.fillStyle = ctx.fillStyle = `#${(`00000${(bgColor | 0).toString(16)}`).substr(-6)}`;
ctx.fillRect(0, 0, avatarCanvas.width, avatarCanvas.height);
ctx.fillStyle = null;
}
let partCount = (bodyParts.length - 1);
while(partCount >= 0)
@ -296,7 +323,6 @@ export class AvatarImage implements IAvatarImage
point.y += avatarCanvas.regPoint.y;
ctx.save();
ctx.scale(scale, 1);
ctx.drawImage(part.image, point.x, point.y, part.image.width, part.image.height);
ctx.restore();
}
@ -305,6 +331,11 @@ export class AvatarImage implements IAvatarImage
partCount--;
}
if(scale !== 1) return CanvasUtilities.scaleCanvas(canvas, scale, scale);
// canvas.width *= scale;
// canvas.height *= scale;
//CanvasUtilities.cropTransparentPixels(canvas);
//if(this._avatarSpriteData && this._avatarSpriteData.paletteIsGrayscale) this.convertToGrayscale(container);
@ -362,7 +393,10 @@ export class AvatarImage implements IAvatarImage
{
if(k.actionType === AvatarAction.EFFECT)
{
if(!this._effectManager.isAvatarEffectReady(parseInt(k.actionParameter))) await this._effectManager.downloadAvatarEffect(parseInt(k.actionParameter));
if(!this._effectManager.isAvatarEffectReady(parseInt(k.actionParameter)))
{
await this._effectManager.downloadAvatarEffect(parseInt(k.actionParameter));
}
}
}
@ -580,7 +614,7 @@ export class AvatarImage implements IAvatarImage
{
if(!this._sortedActions == null) return;
const _local_3: number = Date.now();
const _local_3: number = 0;
const _local_4: string[] = [];
for(const k of this._sortedActions) _local_4.push(k.actionType);
@ -736,8 +770,8 @@ export class AvatarImage implements IAvatarImage
return this._animationHasResetOnToggle;
}
public get mainAction(): string
public get mainAction(): IActiveActionData
{
return this._mainAction.actionType;
return this._mainAction;
}
}

View File

@ -1,5 +1,4 @@
import fetch from 'node-fetch';
import { IAssetManager, IGraphicAsset, NitroManager } from '../../core';
import { FileUtilities, IAssetManager, IGraphicAsset, NitroManager } from '../../core';
import { Application } from '../Application';
import { AssetAliasCollection } from './alias';
import { AvatarAssetDownloadManager } from './AvatarAssetDownloadManager';
@ -18,7 +17,7 @@ import { IFigurePartSet, IStructureData } from './structure';
export class AvatarRenderManager extends NitroManager implements IAvatarRenderManager
{
private static DEFAULT_FIGURE: string = 'hd-99999-99999';
public static DEFAULT_FIGURE: string = 'hd-99999-99999';
private _aliasCollection: AssetAliasCollection;
@ -87,9 +86,9 @@ export class AvatarRenderManager extends NitroManager implements IAvatarRenderMa
const url = Application.instance.getConfiguration<string>('avatar.actions.url');
const data = await fetch(url);
const data = await FileUtilities.readFileAsString(url);
this._structure.updateActions(await data.json());
this._structure.updateActions(JSON.parse(data));
}
private async loadFigureData(): Promise<void>
@ -100,9 +99,9 @@ export class AvatarRenderManager extends NitroManager implements IAvatarRenderMa
const url = Application.instance.getConfiguration<string>('avatar.figuredata.url');
const data = await fetch(url);
const data = await FileUtilities.readFileAsString(url);
this._structure.figureData.appendJSON(await data.json());
this._structure.figureData.appendJSON(JSON.parse(data));
}
public createFigureContainer(figure: string): IAvatarFigureContainer
@ -307,4 +306,9 @@ export class AvatarRenderManager extends NitroManager implements IAvatarRenderMa
{
return this._avatarAssetDownloadManager;
}
public get effectManager(): EffectAssetDownloadManager
{
return this._effectAssetDownloadManager;
}
}

View File

@ -44,21 +44,23 @@ export class EffectAssetDownloadLibrary
return false;
}
public async downloadAsset(): Promise<void>
public async downloadAsset(): Promise<boolean>
{
if(!this._assets || (this._state === EffectAssetDownloadLibrary.LOADING)) return;
if(this.checkIfAssetLoaded()) return;
if(this.checkIfAssetLoaded()) return true;
this._state = EffectAssetDownloadLibrary.LOADING;
await this._assets.downloadAsset(this._downloadUrl);
if(!await this._assets.downloadAsset(this._downloadUrl)) return false;
const collection = this._assets.getCollection(this._libraryName);
if(collection) this._animation = collection.data.animations;
this._state = EffectAssetDownloadLibrary.LOADED;
return true;
}
public get libraryName(): string

View File

@ -1,5 +1,4 @@
import fetch from 'node-fetch';
import { AdvancedMap, IAssetManager } from '../../core';
import { AdvancedMap, FileUtilities, IAssetManager } from '../../core';
import { Application } from '../Application';
import { AvatarStructure } from './AvatarStructure';
import { EffectAssetDownloadLibrary } from './EffectAssetDownloadLibrary';
@ -27,8 +26,8 @@ export class EffectAssetDownloadManager
{
const url = Application.instance.getConfiguration<string>('avatar.effectmap.url');
const data = await fetch(url);
const json = await data.json();
const data = await FileUtilities.readFileAsString(url);
const json = JSON.parse(data);
this.processEffectMap(json.effects);
@ -118,6 +117,8 @@ export class EffectAssetDownloadManager
{
if(!library || library.isLoaded) return;
await library.downloadAsset();
if(!await library.downloadAsset()) return;
this._structure.registerAnimation(library.animation);
}
}

View File

@ -1,5 +1,6 @@
import { Canvas } from 'canvas';
import { IDisposable, IGraphicAsset } from '../../core';
import { IActiveActionData } from './actions';
import { IAnimationLayerData, IAvatarDataContainer, ISpriteDataContainer } from './animation';
import { IAvatarFigureContainer } from './IAvatarFigureContainer';
import { IPartColor } from './structure';
@ -12,7 +13,7 @@ export interface IAvatarImage extends IDisposable
getScale(): string;
getSprites(): ISpriteDataContainer[];
getLayerData(_arg_1: ISpriteDataContainer): IAnimationLayerData;
getImage(setType: string, hightlight: boolean, scale?: number, cache?: boolean): Promise<Canvas>;
getImage(setType: string, bgColor?: number, hightlight?: boolean, scale?: number, cache?: boolean): Promise<Canvas>;
getAsset(_arg_1: string): IGraphicAsset;
getDirection(): number;
getFigure(): IAvatarFigureContainer;
@ -27,5 +28,6 @@ export interface IAvatarImage extends IDisposable
forceActionUpdate(): void;
animationHasResetOnToggle: boolean;
resetAnimationFrameCounter(): void;
mainAction: string;
mainAction: IActiveActionData;
getTotalFrameCount(): number;
}

View File

@ -1,6 +1,7 @@
import { IAssetManager, IGraphicAsset, INitroManager } from '../../core';
import { AvatarAssetDownloadManager } from './AvatarAssetDownloadManager';
import { AvatarStructure } from './AvatarStructure';
import { EffectAssetDownloadManager } from './EffectAssetDownloadManager';
import { IAvatarFigureContainer } from './IAvatarFigureContainer';
import { IAvatarImage } from './IAvatarImage';
import { IStructureData } from './structure/IStructureData';
@ -20,4 +21,5 @@ export interface IAvatarRenderManager extends INitroManager
structure: AvatarStructure;
structureData: IStructureData;
downloadManager: AvatarAssetDownloadManager;
effectManager: EffectAssetDownloadManager;
}

View File

@ -10,7 +10,7 @@ export class AvatarImageActionCache
{
this._cache = new AdvancedMap();
this.setLastAccessTime(Date.now());
this.setLastAccessTime(0);
}
public dispose(): void

View File

@ -1,5 +1,4 @@
import { createCanvas } from 'canvas';
import { AdvancedMap, Point, Rectangle } from '../../../core';
import { AdvancedMap, CanvasUtilities, Point, Rectangle } from '../../../core';
import { IActiveActionData } from '../actions';
import { AssetAliasCollection } from '../alias';
import { AvatarAnimationLayerData } from '../animation';
@ -78,7 +77,7 @@ export class AvatarImageCache
public disposeInactiveActions(k: number = 60000): void
{
const time = Date.now();
const time = 0;
if(this._cache)
{
@ -437,7 +436,7 @@ export class AvatarImageCache
for(const data of imageDatas) data && bounds.enlarge(data.offsetRect);
const point = new Point(-(bounds.x), -(bounds.y));
const canvas = createCanvas(bounds.width, bounds.height);
const canvas = CanvasUtilities.createNitroCanvas(bounds.width, bounds.height);
const ctx = canvas.getContext('2d');
for(const data of imageDatas)
@ -477,10 +476,6 @@ export class AvatarImageCache
ctx.transform(scale, 0, 0, 1, tx, ty);
ctx.drawImage(tintedTexture, 0, 0, data.rect.width, data.rect.height);
ctx.restore();
// set the color
//console.log(canvas.toDataURL());
//console.log();
}
return new CompleteImageData(canvas, new Rectangle(0, 0, canvas.width, canvas.height), point, isFlipped, null);

View File

@ -12,17 +12,17 @@ export const HabboAvatarGeometry = {
'geometries': [
{
'id': 'vertical',
'width': 90,
'height': 130,
'width': 64,
'height': 110,
'dx': 0,
'dy': 0
'dy': 6
},
{
'id': 'sitting',
'width': 90,
'height': 130,
'width': 64,
'height': 110,
'dx': 0,
'dy': 0
'dy': 6
},
{
'id': 'horizontal',

View File

@ -0,0 +1,6 @@
import { Router } from 'express';
import { HabboImagingRouter } from './habbo-imaging';
export const HttpRouter = Router();
HttpRouter.use('/', HabboImagingRouter);

View File

@ -0,0 +1,6 @@
import { Router } from 'express';
import { HabboImagingRouterGet } from './handlers';
export const HabboImagingRouter = Router();
HabboImagingRouter.get('/', HabboImagingRouterGet);

View File

@ -0,0 +1,236 @@
import { Canvas, createCanvas } from 'canvas';
import { Request, Response } from 'express';
import { createWriteStream, writeFile, WriteStream } from 'fs';
import * as GIFEncoder from 'gifencoder';
import { File, FileUtilities, Point } from '../../../../core';
import { Application } from '../../../Application';
import { AvatarScaleType, IAvatarImage } from '../../../avatar';
import { BuildFigureOptionsRequest, BuildFigureOptionsStringRequest, ProcessActionRequest, ProcessDanceRequest, ProcessDirectionRequest, ProcessEffectRequest, ProcessGestureRequest, RequestQuery } from './utils';
export const HabboImagingRouterGet = async (request: Request<any, any, any, RequestQuery>, response: Response) =>
{
const query = request.query;
try
{
const buildOptions = BuildFigureOptionsRequest(query);
const saveDirectory = Application.instance.getConfiguration<string>('avatar.save.path');
const directory = FileUtilities.getDirectory(saveDirectory);
const avatarString = BuildFigureOptionsStringRequest(buildOptions);
const saveFile = new File(`${ directory.path }/${ avatarString }.${ buildOptions.imageFormat }`);
if(saveFile.exists())
{
const buffer = await FileUtilities.readFileAsBuffer(saveFile.path);
if(buffer)
{
response
.writeHead(200, {
'Content-Type': ((buildOptions.imageFormat === 'gif') ? 'image/gif' : 'image/png')
})
.end(buffer);
}
return;
}
if(buildOptions.effect > 0)
{
if(!Application.instance.avatar.effectManager.isAvatarEffectReady(buildOptions.effect))
{
await Application.instance.avatar.effectManager.downloadAvatarEffect(buildOptions.effect);
}
}
const avatar = await Application.instance.avatar.createAvatarImage(buildOptions.figure, AvatarScaleType.LARGE, 'M');
const avatarCanvas = Application.instance.avatar.structure.getCanvas(avatar.getScale(), avatar.mainAction.definition.geometryType);
ProcessDirectionRequest(query, avatar);
avatar.initActionAppends();
ProcessActionRequest(query, avatar);
ProcessGestureRequest(query, avatar);
ProcessDanceRequest(query, avatar);
ProcessEffectRequest(query, avatar);
avatar.endActionAppends();
const bgColor = 376510773; // magenta
const tempCanvas = createCanvas((avatarCanvas.width * buildOptions.size), (avatarCanvas.height * buildOptions.size));
const tempCtx = tempCanvas.getContext('2d');
let encoder: GIFEncoder = null;
let stream: WriteStream = null;
if(buildOptions.imageFormat === 'gif')
{
encoder = new GIFEncoder(tempCanvas.width, tempCanvas.height);
stream = encoder.createReadStream().pipe(createWriteStream(saveFile.path));
encoder.setTransparent(bgColor);
encoder.start();
encoder.setRepeat(0);
encoder.setDelay(1);
encoder.setQuality(10);
}
let totalFrames = 0;
if(buildOptions.imageFormat !== 'gif')
{
if(buildOptions.frameNumber > 0) avatar.updateAnimationByFrames(buildOptions.frameNumber);
totalFrames = 1;
}
else
{
totalFrames = ((avatar.getTotalFrameCount() * 2) || 1);
}
for(let i = 0; i < totalFrames; i++)
{
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
if(totalFrames && (i > 0)) avatar.updateAnimationByFrames(1);
const canvas = await avatar.getImage(buildOptions.setType, 0, false, buildOptions.size);
const avatarOffset = new Point();
const canvasOffset = new Point();
canvasOffset.x = ((tempCanvas.width - canvas.width) / 2);
canvasOffset.y = ((tempCanvas.height - canvas.height) / 2);
for(const sprite of avatar.getSprites())
{
if(sprite.id === 'avatar')
{
const layerData = avatar.getLayerData(sprite);
avatarOffset.x = sprite.getDirectionOffsetX(buildOptions.direction);
avatarOffset.y = sprite.getDirectionOffsetY(buildOptions.direction);
if(layerData)
{
avatarOffset.x += layerData.dx;
avatarOffset.y += layerData.dy;
}
}
}
const avatarSize = 64;
const sizeOffset = new Point(((canvas.width - avatarSize) / 2), (canvas.height - (avatarSize / 4)));
ProcessAvatarSprites(tempCanvas, avatar, avatarOffset, canvasOffset.add(sizeOffset), false);
tempCtx.drawImage(canvas, avatarOffset.x, avatarOffset.y, canvas.width, canvas.height);
ProcessAvatarSprites(tempCanvas, avatar, avatarOffset, canvasOffset.add(sizeOffset), true);
if(encoder)
{
encoder.addFrame(tempCtx);
}
else
{
const buffer = tempCanvas.toBuffer();
response
.writeHead(200, {
'Content-Type': 'image/png'
})
.end(buffer);
writeFile(saveFile.path, buffer, () => {});
return;
}
}
if(encoder) encoder.finish();
if(stream)
{
await new Promise((resolve, reject) =>
{
stream.on('finish', resolve);
stream.on('error', reject);
});
}
const buffer = await FileUtilities.readFileAsBuffer(saveFile.path);
response
.writeHead(200, {
'Content-Type': 'image/gif'
})
.end(buffer);
}
catch(err)
{
Application.instance.logger.error(err.message);
response
.writeHead(500)
.end();
}
}
function ProcessAvatarSprites(canvas: Canvas, avatar: IAvatarImage, avatarOffset: Point, canvasOffset: Point, frontSprites: boolean = true)
{
const ctx = canvas.getContext('2d');
for(const sprite of avatar.getSprites())
{
if(sprite.id === 'avatar') continue;
const layerData = avatar.getLayerData(sprite);
let offsetX = sprite.getDirectionOffsetX(avatar.getDirection());
let offsetY = sprite.getDirectionOffsetY(avatar.getDirection());
let offsetZ = sprite.getDirectionOffsetZ(avatar.getDirection());
let direction = 0;
let frame = 0;
if(!frontSprites)
{
if(offsetZ >= 0) continue;
}
else if(offsetZ < 0) continue;
if(sprite.hasDirections) direction = avatar.getDirection();
if(layerData)
{
frame = layerData.animationFrame;
offsetX = (offsetX + layerData.dx);
offsetY = (offsetY + layerData.dy);
direction = (direction + layerData.dd);
}
if(direction < 0) direction = (direction + 8);
if(direction > 7) direction = (direction - 8);
const assetName = ((((((avatar.getScale() + "_") + sprite.member) + "_") + direction) + "_") + frame);
const asset = avatar.getAsset(assetName);
if(!asset) continue;
const texture = asset.texture;
let x = ((canvasOffset.x - (1 * asset.offsetX)) + offsetX);
let y = ((canvasOffset.y - (1 * asset.offsetY)) + offsetY);
ctx.save();
if(sprite.ink === 33) ctx.globalCompositeOperation = 'lighter';
ctx.transform(1, 0, 0, 1, (x - avatarOffset.x), (y - avatarOffset.y));
ctx.drawImage(texture.drawableCanvas, 0, 0, texture.width, texture.height);
ctx.restore();
}
}

View File

@ -0,0 +1 @@
export * from './HabboImagingRouterGet';

View File

@ -0,0 +1,42 @@
import { GetActionRequest } from './GetActionRequest';
import { GetDanceRequest } from './GetDanceRequest';
import { GetDirectionRequest } from './GetDirectionRequest';
import { GetEffectRequest } from './GetEffectRequest';
import { GetFigureRequest } from './GetFigureRequest';
import { GetFrameNumberRequest } from './GetFrameNumberRequest';
import { GetGestureRequest } from './GetGestureRequest';
import { GetHeadDirectionRequest } from './GetHeadDirectionRequest';
import { GetImageFormatRequest } from './GetImageFormatRequest';
import { GetSetTypeRequest } from './GetSetTypeRequest';
import { GetSizeRequest } from './GetSizeRequest';
import { IFigureBuildOptions } from './IFigureBuildOptions';
import { RequestQuery } from './RequestQuery';
export const BuildFigureOptionsRequest = (query: RequestQuery) =>
{
const figure = GetFigureRequest(query);
const size = GetSizeRequest(query);
const setType = GetSetTypeRequest(query);
const direction = (GetDirectionRequest(query) || 2);
const headDirection = (GetHeadDirectionRequest(query) || direction);
const action = GetActionRequest(query);
const gesture = GetGestureRequest(query);
const dance = GetDanceRequest(query);
const effect = GetEffectRequest(query);
const frameNumber = GetFrameNumberRequest(query);
const imageFormat = GetImageFormatRequest(query);
return {
figure,
size,
setType,
direction,
headDirection,
action,
gesture,
dance,
effect,
frameNumber,
imageFormat
} as IFigureBuildOptions;
}

View File

@ -0,0 +1,22 @@
import { IFigureBuildOptions } from './IFigureBuildOptions';
const PART_SEPARATOR = '.';
export const BuildFigureOptionsStringRequest = (buildOptions: IFigureBuildOptions) =>
{
let buildString = '';
if(buildOptions.figure) buildString += buildOptions.figure;
if(buildOptions.size) buildString += PART_SEPARATOR + 's-' + buildOptions.size;
if(buildOptions.setType) buildString += PART_SEPARATOR + 'st-' + buildOptions.setType;
if(buildOptions.direction) buildString += PART_SEPARATOR + 'd-' + buildOptions.direction;
if(buildOptions.headDirection) buildString += PART_SEPARATOR + 'hd-' + buildOptions.headDirection;
if(buildOptions.action) buildString += PART_SEPARATOR + 'a-' + buildOptions.action;
if(buildOptions.gesture) buildString += PART_SEPARATOR + 'g-' + buildOptions.gesture;
if(buildOptions.dance) buildString += PART_SEPARATOR + 'da-' + buildOptions.dance;
if(buildOptions.effect) buildString += PART_SEPARATOR + 'fx-' + buildOptions.effect;
if(buildOptions.frameNumber) buildString += PART_SEPARATOR + 'fn-' + buildOptions.frameNumber;
if(buildOptions.imageFormat) buildString += PART_SEPARATOR + 'f-' + buildOptions.imageFormat;
return buildString;
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetActionRequest = (query: RequestQuery) =>
{
return ((query.action && query.action.length) ? query.action : null);
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetDanceRequest = (query: RequestQuery) =>
{
return ((query.dance && query.dance.length) ? parseInt(query.dance) : null);
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetDirectionRequest = (query: RequestQuery) =>
{
return ((query.direction && query.direction.length) ? parseInt(query.direction) : null);
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetEffectRequest = (query: RequestQuery) =>
{
return ((query.effect && query.effect.length) ? parseInt(query.effect) : null);
}

View File

@ -0,0 +1,11 @@
import { AvatarRenderManager } from '../../../../avatar';
import { RequestQuery } from './RequestQuery';
export const GetFigureRequest = (query: RequestQuery) =>
{
let figure = AvatarRenderManager.DEFAULT_FIGURE;
if(query.figure && query.figure.length) figure = query.figure;
return figure;
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetFrameNumberRequest = (query: RequestQuery) =>
{
return ((query.frame_num && query.frame_num.length) ? parseInt(query.frame_num) : -1);
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetGestureRequest = (query: RequestQuery) =>
{
return ((query.gesture && query.gesture.length) ? query.gesture : null);
}

View File

@ -0,0 +1,6 @@
import { RequestQuery } from './RequestQuery';
export const GetHeadDirectionRequest = (query: RequestQuery) =>
{
return ((query.head_direction && query.head_direction.length) ? parseInt(query.head_direction) : null);
}

View File

@ -0,0 +1,8 @@
import { RequestQuery } from './RequestQuery';
export const GetImageFormatRequest = (query: RequestQuery) =>
{
if(query.img_format === 'gif') return 'gif';
return 'png';
}

View File

@ -0,0 +1,7 @@
import { AvatarScaleType } from '../../../../avatar';
import { RequestQuery } from './RequestQuery';
export const GetScaleRequest = (query: RequestQuery) =>
{
return AvatarScaleType.LARGE;
}

View File

@ -0,0 +1,11 @@
import { AvatarSetType } from '../../../../avatar';
import { RequestQuery } from './RequestQuery';
export const GetSetTypeRequest = (query: RequestQuery) =>
{
let setType = AvatarSetType.FULL;
if(query.headonly && query.headonly == '1') setType = AvatarSetType.HEAD;
return setType;
}

View File

@ -0,0 +1,10 @@
import { RequestQuery } from './RequestQuery';
export const GetSizeRequest = (query: RequestQuery) =>
{
if(query.size === 's') return 0.5;
if(query.size === 'l') return 2;
return 1;
}

View File

@ -0,0 +1,14 @@
export interface IFigureBuildOptions
{
figure: string;
size: number;
setType: string;
direction: number;
headDirection: number;
action: string;
gesture: string;
dance: number;
effect: number;
frameNumber: number;
imageFormat: string;
}

View File

@ -0,0 +1,14 @@
export interface RequestQuery
{
figure: string;
size: string;
action: string;
headonly: string;
gesture: string;
direction: string;
head_direction: string;
dance: string;
effect: string;
frame_num: string;
img_format: string;
}

View File

@ -0,0 +1,20 @@
import { IAvatarImage } from '../../../../../avatar';
import { GetActionRequest } from '../GetActionRequest';
import { RequestQuery } from '../RequestQuery';
import { ProcessCarryAction } from './ProcessCarryAction';
import { ProcessExpressionAction } from './ProcessExpressionAction';
import { ProcessPostureAction } from './ProcessPostureAction';
export const ProcessActionRequest = (query: RequestQuery, avatar: IAvatarImage) =>
{
const actions = (GetActionRequest(query)?.split(',') || []);
for(const action of actions)
{
if(ProcessPostureAction(action, avatar)) continue;
if(ProcessExpressionAction(action, avatar)) continue;
if(ProcessCarryAction(action, avatar)) continue;
}
}

View File

@ -0,0 +1,34 @@
import { AvatarAction, IAvatarImage } from '../../../../../avatar';
export const ProcessCarryAction = (action: string, avatar: IAvatarImage) =>
{
let didSet = false;
let carryType: string = null;
let param: string = null;
if(action && action.length)
{
const [ key, value ] = action.split('=');
if(value && value.length) param = value;
switch(key)
{
case 'crr':
case AvatarAction.CARRY_OBJECT:
didSet = true;
carryType = AvatarAction.CARRY_OBJECT;
break;
case 'drk':
case AvatarAction.USE_OBJECT:
didSet = true;
carryType = AvatarAction.USE_OBJECT;
break;
}
}
if(carryType && carryType.length && param && param.length) avatar.appendAction(carryType, param);
return didSet;
}

View File

@ -0,0 +1,40 @@
import { AvatarAction, IAvatarImage } from '../../../../../avatar';
export const ProcessExpressionAction = (action: string, avatar: IAvatarImage) =>
{
let didSet = false;
let expression: string = null;
let param: string = null;
if(action && action.length)
{
const [ key, value ] = action.split('=');
if(value && value.length) param = value;
switch(key)
{
case 'wav':
case AvatarAction.EXPRESSION_WAVE:
didSet = true;
expression = AvatarAction.EXPRESSION_WAVE;
break;
case AvatarAction.EXPRESSION_BLOW_A_KISS:
case AvatarAction.EXPRESSION_CRY:
case AvatarAction.EXPRESSION_IDLE:
case AvatarAction.EXPRESSION_LAUGH:
case AvatarAction.EXPRESSION_RESPECT:
case AvatarAction.EXPRESSION_RIDE_JUMP:
case AvatarAction.EXPRESSION_SNOWBOARD_OLLIE:
case AvatarAction.EXPRESSION_SNOWBORD_360:
didSet = true;
expression = key;
break;
}
}
if(expression && expression.length) avatar.appendAction(expression);
return didSet;
}

View File

@ -0,0 +1,35 @@
import { AvatarAction, IAvatarImage } from '../../../../../avatar';
export const ProcessPostureAction = (action: string, avatar: IAvatarImage) =>
{
let didSet = false;
let posture = AvatarAction.POSTURE_STAND;
let param = null;
if(action && action.length)
{
const [ key, value ] = action.split('=');
if(value && value.length) param = value;
switch(key)
{
case 'wlk':
case AvatarAction.POSTURE_WALK:
didSet = true;
posture = AvatarAction.POSTURE_WALK;
break;
case AvatarAction.POSTURE_SIT:
case AvatarAction.POSTURE_LAY:
case AvatarAction.POSTURE_STAND:
didSet = true;
posture = key;
break;
}
}
if(posture && posture.length) avatar.appendAction(AvatarAction.POSTURE, posture, param);
return didSet;
}

View File

@ -0,0 +1,4 @@
export * from './ProcessActionRequest';
export * from './ProcessCarryAction';
export * from './ProcessExpressionAction';
export * from './ProcessPostureAction';

View File

@ -0,0 +1,20 @@
import { AvatarAction, IAvatarImage } from '../../../../../avatar';
import { GetDanceRequest } from '../GetDanceRequest';
import { RequestQuery } from '../RequestQuery';
export const ProcessDanceRequest = (query: RequestQuery, avatar: IAvatarImage) =>
{
const dance: number = (GetDanceRequest(query) || null);
if(!dance) return;
switch(dance)
{
case 1:
case 2:
case 3:
case 4:
avatar.appendAction(AvatarAction.DANCE, dance);
return;
}
}

View File

@ -0,0 +1 @@
export * from './ProcessDanceRequest';

View File

@ -0,0 +1,13 @@
import { AvatarSetType, IAvatarImage } from '../../../../../avatar';
import { GetDirectionRequest } from '../GetDirectionRequest';
import { GetHeadDirectionRequest } from '../GetHeadDirectionRequest';
import { RequestQuery } from '../RequestQuery';
export const ProcessDirectionRequest = (query: RequestQuery, avatar: IAvatarImage) =>
{
const direction = (GetDirectionRequest(query) || 2);
const headDirection = (GetHeadDirectionRequest(query) || direction);
avatar.setDirection(AvatarSetType.FULL, direction);
avatar.setDirection(AvatarSetType.HEAD, headDirection);
}

View File

@ -0,0 +1 @@
export * from './ProcessDirectionRequest';

View File

@ -0,0 +1,12 @@
import { AvatarAction, IAvatarImage } from '../../../../../avatar';
import { GetEffectRequest } from '../GetEffectRequest';
import { RequestQuery } from '../RequestQuery';
export const ProcessEffectRequest = (query: RequestQuery, avatar: IAvatarImage) =>
{
const effect: number = (GetEffectRequest(query) || null);
if(!effect) return;
avatar.appendAction(AvatarAction.EFFECT, effect);
}

View File

@ -0,0 +1 @@
export * from './ProcessEffectRequest';

View File

@ -0,0 +1,27 @@
import { AvatarAction, IAvatarImage } from '../../../../../avatar';
import { GetGestureRequest } from '../GetGestureRequest';
import { RequestQuery } from '../RequestQuery';
export const ProcessGestureRequest = (query: RequestQuery, avatar: IAvatarImage) =>
{
const gesture: string = (GetGestureRequest(query) || null);
if(!gesture) return;
switch(gesture)
{
case AvatarAction.POSTURE_STAND:
case AvatarAction.GESTURE_AGGRAVATED:
case AvatarAction.GESTURE_SAD:
case AvatarAction.GESTURE_SMILE:
case AvatarAction.GESTURE_SURPRISED:
avatar.appendAction(AvatarAction.GESTURE, gesture);
return;
case 'spk':
avatar.appendAction(AvatarAction.TALK);
return;
case 'eyb':
avatar.appendAction(AvatarAction.SLEEP);
return;
}
}

View File

@ -0,0 +1 @@
export * from './ProcessGestureRequest';

View File

@ -0,0 +1,21 @@
export * from './action';
export * from './BuildFigureOptionsRequest';
export * from './BuildFigureOptionsStringRequest';
export * from './dance';
export * from './direction';
export * from './effect';
export * from './gesture';
export * from './GetActionRequest';
export * from './GetDanceRequest';
export * from './GetDirectionRequest';
export * from './GetEffectRequest';
export * from './GetFigureRequest';
export * from './GetFrameNumberRequest';
export * from './GetGestureRequest';
export * from './GetHeadDirectionRequest';
export * from './GetImageFormatRequest';
export * from './GetScaleRequest';
export * from './GetSetTypeRequest';
export * from './GetSizeRequest';
export * from './IFigureBuildOptions';
export * from './RequestQuery';

View File

@ -0,0 +1 @@
export * from './HabboImagingRouter';

View File

@ -18,9 +18,17 @@ export class NitroCore extends NitroManager implements INitroCore
protected async onInit(): Promise<void>
{
if(this._configuration) await this._configuration.init();
try
{
if(this._configuration) await this._configuration.init();
if(this._asset) await this._asset.init();
if(this._asset) await this._asset.init();
}
catch(err)
{
this.logger.error(err.message || err);
}
}
protected async onDispose(): Promise<void>

View File

@ -96,6 +96,7 @@ export class AssetManager extends NitroManager implements IAssetManager
{
try
{
this.logger.log('Downloading: ' + url);
const buffer = await FileUtilities.readFileAsBuffer(url);
const bundle = await NitroBundle.from(buffer);

View File

@ -25,7 +25,17 @@ export class NitroManager extends Disposable implements INitroManager
this._isLoading = true;
await this.onInit();
try
{
await this.onInit();
}
catch(err)
{
this.logger.error(err.message || err);
return;
}
this._isLoaded = true;
this._isLoading = false;

View File

@ -1,6 +1,5 @@
import fetch from 'node-fetch';
import { NitroManager } from '../common';
import { AdvancedMap } from '../utils';
import { NitroManager } from '../common';
import { AdvancedMap, FileUtilities } from '../utils';
import { IConfigurationManager } from './IConfigurationManager';
export class ConfigurationManager extends NitroManager implements IConfigurationManager
@ -16,17 +15,17 @@ export class ConfigurationManager extends NitroManager implements IConfiguration
protected async onInit(): Promise<void>
{
await this.loadConfigurationFromUrl((process.env.CONFIG_URL || null));
await this.loadConfigurationFromUrl('./config.json');
}
private async loadConfigurationFromUrl(url: string): Promise<void>
{
if(!url || (url === '')) return Promise.reject('invalid_config_url');
if(!url || (url === '')) throw new Error(`Invalid configuration url: ${ url }`);
try
{
const response = await fetch(url);
const json = await response.json();
const response = await FileUtilities.readFileAsString(url);
const json = JSON.parse(response);
if(!this.parseConfiguration(json)) return Promise.reject('invalid_config');
}

View File

@ -99,4 +99,25 @@ export class CanvasUtilities
return canvas;
}
public static scaleCanvas(canvas: Canvas, scaleX: number, scaleY: number): Canvas
{
const tempCanvas = this.createNitroCanvas((canvas.width * scaleX), (canvas.height * scaleY));
const ctx = tempCanvas.getContext('2d');
ctx.scale(scaleX, scaleY);
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height);
return tempCanvas;
}
public static createNitroCanvas(width: number, height: number): Canvas
{
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
return canvas;
}
}

View File

@ -14,13 +14,6 @@ export class Point
return new Point(this.x, this.y);
}
public copyFrom(p: Point): Point
{
this.add(p.x, p.y);
return this;
}
public equals(p: Point): boolean
{
return ((p.x === this.x) && (p.y === this.y));
@ -33,4 +26,14 @@ export class Point
return this;
}
public add(point: Point): Point
{
const clone = this.clone();
clone.x += point.x;
clone.y += point.y;
return clone;
}
}

View File

@ -1,5 +1,3 @@
require('dotenv').config();
import { Application } from './app';
import { NitroCore } from './core';

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"target": "es6",
"sourceMap": false,
"allowJs": false,
"baseUrl": "./src",
"outDir": "./dist"
},
"include": [
"src",
"index.ts",
"config.json"
],
"exclude": [
"node_modules",
"dist"
]
}