diff --git a/.env b/.env deleted file mode 100644 index 464c1e1..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -CONFIG_URL=http://client.nitrots.co:3000/renderer-config.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d511ea --- /dev/null +++ b/README.md @@ -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. diff --git a/config.json b/config.json new file mode 100644 index 0000000..2b2daf4 --- /dev/null +++ b/config.json @@ -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" + } + ] + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..7b3383c --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +require('./src/main'); diff --git a/package-lock.json b/package-lock.json index 5b7b207..61eb530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 24d8358..8c38ab3 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app/Application.ts b/src/app/Application.ts index 03f9690..1caf535 100644 --- a/src/app/Application.ts +++ b/src/app/Application.ts @@ -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(key, value); } + private setupRouter(): void + { + const router = express(); + + router.use('/', HttpRouter); + + const host = this.getConfiguration('api.host'); + const port = this.getConfiguration('api.port'); + + router.listen(port, host, () => + { + this.logger.log(`Server Started ${ host }:${ port }`); + }); + } + public get core(): INitroCore { return this._core; diff --git a/src/app/avatar/AvatarAssetDownloadLibrary.ts b/src/app/avatar/AvatarAssetDownloadLibrary.ts index a9a0ba4..8efe003 100644 --- a/src/app/avatar/AvatarAssetDownloadLibrary.ts +++ b/src/app/avatar/AvatarAssetDownloadLibrary.ts @@ -42,17 +42,19 @@ export class AvatarAssetDownloadLibrary return false; } - public async downloadAsset(): Promise + public async downloadAsset(): Promise { - 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 diff --git a/src/app/avatar/AvatarAssetDownloadManager.ts b/src/app/avatar/AvatarAssetDownloadManager.ts index 5ae50db..f1f9045 100644 --- a/src/app/avatar/AvatarAssetDownloadManager.ts +++ b/src/app/avatar/AvatarAssetDownloadManager.ts @@ -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('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; } } diff --git a/src/app/avatar/AvatarImage.ts b/src/app/avatar/AvatarImage.ts index 66cac9a..ddff710 100644 --- a/src/app/avatar/AvatarImage.ts +++ b/src/app/avatar/AvatarImage.ts @@ -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 + public async getImage(setType: string, bgColor: number = 0, hightlight: boolean = false, scale: number = 1): Promise { 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; } } diff --git a/src/app/avatar/AvatarRenderManager.ts b/src/app/avatar/AvatarRenderManager.ts index a0e458d..79fe1d4 100644 --- a/src/app/avatar/AvatarRenderManager.ts +++ b/src/app/avatar/AvatarRenderManager.ts @@ -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('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 @@ -100,9 +99,9 @@ export class AvatarRenderManager extends NitroManager implements IAvatarRenderMa const url = Application.instance.getConfiguration('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; + } } diff --git a/src/app/avatar/EffectAssetDownloadLibrary.ts b/src/app/avatar/EffectAssetDownloadLibrary.ts index bfd16f9..22f238e 100644 --- a/src/app/avatar/EffectAssetDownloadLibrary.ts +++ b/src/app/avatar/EffectAssetDownloadLibrary.ts @@ -44,21 +44,23 @@ export class EffectAssetDownloadLibrary return false; } - public async downloadAsset(): Promise + public async downloadAsset(): Promise { 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 diff --git a/src/app/avatar/EffectAssetDownloadManager.ts b/src/app/avatar/EffectAssetDownloadManager.ts index 5360ac4..f070b04 100644 --- a/src/app/avatar/EffectAssetDownloadManager.ts +++ b/src/app/avatar/EffectAssetDownloadManager.ts @@ -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('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); } } diff --git a/src/app/avatar/IAvatarImage.ts b/src/app/avatar/IAvatarImage.ts index 4bf5cce..d0aa9d7 100644 --- a/src/app/avatar/IAvatarImage.ts +++ b/src/app/avatar/IAvatarImage.ts @@ -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; + getImage(setType: string, bgColor?: number, hightlight?: boolean, scale?: number, cache?: boolean): Promise; 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; } diff --git a/src/app/avatar/IAvatarRenderManager.ts b/src/app/avatar/IAvatarRenderManager.ts index bf630cd..a0c52ae 100644 --- a/src/app/avatar/IAvatarRenderManager.ts +++ b/src/app/avatar/IAvatarRenderManager.ts @@ -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; } diff --git a/src/app/avatar/cache/AvatarImageActionCache.ts b/src/app/avatar/cache/AvatarImageActionCache.ts index b65161b..8f878ce 100644 --- a/src/app/avatar/cache/AvatarImageActionCache.ts +++ b/src/app/avatar/cache/AvatarImageActionCache.ts @@ -10,7 +10,7 @@ export class AvatarImageActionCache { this._cache = new AdvancedMap(); - this.setLastAccessTime(Date.now()); + this.setLastAccessTime(0); } public dispose(): void diff --git a/src/app/avatar/cache/AvatarImageCache.ts b/src/app/avatar/cache/AvatarImageCache.ts index e16f374..96d7fef 100644 --- a/src/app/avatar/cache/AvatarImageCache.ts +++ b/src/app/avatar/cache/AvatarImageCache.ts @@ -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); diff --git a/src/app/avatar/data/HabboAvatarGeometry.ts b/src/app/avatar/data/HabboAvatarGeometry.ts index e6b9db0..55c70b6 100644 --- a/src/app/avatar/data/HabboAvatarGeometry.ts +++ b/src/app/avatar/data/HabboAvatarGeometry.ts @@ -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', diff --git a/src/app/router/HttpRouter.ts b/src/app/router/HttpRouter.ts new file mode 100644 index 0000000..4166323 --- /dev/null +++ b/src/app/router/HttpRouter.ts @@ -0,0 +1,6 @@ +import { Router } from 'express'; +import { HabboImagingRouter } from './habbo-imaging'; + +export const HttpRouter = Router(); + +HttpRouter.use('/', HabboImagingRouter); diff --git a/src/app/router/habbo-imaging/HabboImagingRouter.ts b/src/app/router/habbo-imaging/HabboImagingRouter.ts new file mode 100644 index 0000000..fa3883f --- /dev/null +++ b/src/app/router/habbo-imaging/HabboImagingRouter.ts @@ -0,0 +1,6 @@ +import { Router } from 'express'; +import { HabboImagingRouterGet } from './handlers'; + +export const HabboImagingRouter = Router(); + +HabboImagingRouter.get('/', HabboImagingRouterGet); diff --git a/src/app/router/habbo-imaging/handlers/HabboImagingRouterGet.ts b/src/app/router/habbo-imaging/handlers/HabboImagingRouterGet.ts new file mode 100644 index 0000000..d637916 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/HabboImagingRouterGet.ts @@ -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, response: Response) => +{ + const query = request.query; + + try + { + const buildOptions = BuildFigureOptionsRequest(query); + const saveDirectory = Application.instance.getConfiguration('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(); + } +} diff --git a/src/app/router/habbo-imaging/handlers/index.ts b/src/app/router/habbo-imaging/handlers/index.ts new file mode 100644 index 0000000..d60024d --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/index.ts @@ -0,0 +1 @@ +export * from './HabboImagingRouterGet'; diff --git a/src/app/router/habbo-imaging/handlers/utils/BuildFigureOptionsRequest.ts b/src/app/router/habbo-imaging/handlers/utils/BuildFigureOptionsRequest.ts new file mode 100644 index 0000000..7db9b25 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/BuildFigureOptionsRequest.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/BuildFigureOptionsStringRequest.ts b/src/app/router/habbo-imaging/handlers/utils/BuildFigureOptionsStringRequest.ts new file mode 100644 index 0000000..6a040d8 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/BuildFigureOptionsStringRequest.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetActionRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetActionRequest.ts new file mode 100644 index 0000000..609be72 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetActionRequest.ts @@ -0,0 +1,6 @@ +import { RequestQuery } from './RequestQuery'; + +export const GetActionRequest = (query: RequestQuery) => +{ + return ((query.action && query.action.length) ? query.action : null); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetDanceRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetDanceRequest.ts new file mode 100644 index 0000000..d211b51 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetDanceRequest.ts @@ -0,0 +1,6 @@ +import { RequestQuery } from './RequestQuery'; + +export const GetDanceRequest = (query: RequestQuery) => +{ + return ((query.dance && query.dance.length) ? parseInt(query.dance) : null); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetDirectionRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetDirectionRequest.ts new file mode 100644 index 0000000..b26dac1 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetDirectionRequest.ts @@ -0,0 +1,6 @@ +import { RequestQuery } from './RequestQuery'; + +export const GetDirectionRequest = (query: RequestQuery) => +{ + return ((query.direction && query.direction.length) ? parseInt(query.direction) : null); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetEffectRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetEffectRequest.ts new file mode 100644 index 0000000..d13c0a0 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetEffectRequest.ts @@ -0,0 +1,6 @@ +import { RequestQuery } from './RequestQuery'; + +export const GetEffectRequest = (query: RequestQuery) => +{ + return ((query.effect && query.effect.length) ? parseInt(query.effect) : null); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetFigureRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetFigureRequest.ts new file mode 100644 index 0000000..368adab --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetFigureRequest.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetFrameNumberRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetFrameNumberRequest.ts new file mode 100644 index 0000000..4722bf3 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetFrameNumberRequest.ts @@ -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); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetGestureRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetGestureRequest.ts new file mode 100644 index 0000000..ac26b28 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetGestureRequest.ts @@ -0,0 +1,6 @@ +import { RequestQuery } from './RequestQuery'; + +export const GetGestureRequest = (query: RequestQuery) => +{ + return ((query.gesture && query.gesture.length) ? query.gesture : null); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetHeadDirectionRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetHeadDirectionRequest.ts new file mode 100644 index 0000000..f20127f --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetHeadDirectionRequest.ts @@ -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); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetImageFormatRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetImageFormatRequest.ts new file mode 100644 index 0000000..53705f5 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetImageFormatRequest.ts @@ -0,0 +1,8 @@ +import { RequestQuery } from './RequestQuery'; + +export const GetImageFormatRequest = (query: RequestQuery) => +{ + if(query.img_format === 'gif') return 'gif'; + + return 'png'; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetScaleRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetScaleRequest.ts new file mode 100644 index 0000000..1e8bc29 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetScaleRequest.ts @@ -0,0 +1,7 @@ +import { AvatarScaleType } from '../../../../avatar'; +import { RequestQuery } from './RequestQuery'; + +export const GetScaleRequest = (query: RequestQuery) => +{ + return AvatarScaleType.LARGE; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetSetTypeRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetSetTypeRequest.ts new file mode 100644 index 0000000..a7f76e6 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetSetTypeRequest.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/GetSizeRequest.ts b/src/app/router/habbo-imaging/handlers/utils/GetSizeRequest.ts new file mode 100644 index 0000000..cd9c110 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/GetSizeRequest.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/IFigureBuildOptions.ts b/src/app/router/habbo-imaging/handlers/utils/IFigureBuildOptions.ts new file mode 100644 index 0000000..f5751a0 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/IFigureBuildOptions.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/RequestQuery.ts b/src/app/router/habbo-imaging/handlers/utils/RequestQuery.ts new file mode 100644 index 0000000..3c76119 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/RequestQuery.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/action/ProcessActionRequest.ts b/src/app/router/habbo-imaging/handlers/utils/action/ProcessActionRequest.ts new file mode 100644 index 0000000..52fe6dd --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/action/ProcessActionRequest.ts @@ -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; + } +} diff --git a/src/app/router/habbo-imaging/handlers/utils/action/ProcessCarryAction.ts b/src/app/router/habbo-imaging/handlers/utils/action/ProcessCarryAction.ts new file mode 100644 index 0000000..20692e0 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/action/ProcessCarryAction.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/action/ProcessExpressionAction.ts b/src/app/router/habbo-imaging/handlers/utils/action/ProcessExpressionAction.ts new file mode 100644 index 0000000..3166d9b --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/action/ProcessExpressionAction.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/action/ProcessPostureAction.ts b/src/app/router/habbo-imaging/handlers/utils/action/ProcessPostureAction.ts new file mode 100644 index 0000000..581368a --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/action/ProcessPostureAction.ts @@ -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; +} diff --git a/src/app/router/habbo-imaging/handlers/utils/action/index.ts b/src/app/router/habbo-imaging/handlers/utils/action/index.ts new file mode 100644 index 0000000..5156d79 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/action/index.ts @@ -0,0 +1,4 @@ +export * from './ProcessActionRequest'; +export * from './ProcessCarryAction'; +export * from './ProcessExpressionAction'; +export * from './ProcessPostureAction'; diff --git a/src/app/router/habbo-imaging/handlers/utils/dance/ProcessDanceRequest.ts b/src/app/router/habbo-imaging/handlers/utils/dance/ProcessDanceRequest.ts new file mode 100644 index 0000000..8a677da --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/dance/ProcessDanceRequest.ts @@ -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; + } +} diff --git a/src/app/router/habbo-imaging/handlers/utils/dance/index.ts b/src/app/router/habbo-imaging/handlers/utils/dance/index.ts new file mode 100644 index 0000000..cb87009 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/dance/index.ts @@ -0,0 +1 @@ +export * from './ProcessDanceRequest'; diff --git a/src/app/router/habbo-imaging/handlers/utils/direction/ProcessDirectionRequest.ts b/src/app/router/habbo-imaging/handlers/utils/direction/ProcessDirectionRequest.ts new file mode 100644 index 0000000..20fc1cf --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/direction/ProcessDirectionRequest.ts @@ -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); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/direction/index.ts b/src/app/router/habbo-imaging/handlers/utils/direction/index.ts new file mode 100644 index 0000000..a9df07e --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/direction/index.ts @@ -0,0 +1 @@ +export * from './ProcessDirectionRequest'; diff --git a/src/app/router/habbo-imaging/handlers/utils/effect/ProcessEffectRequest.ts b/src/app/router/habbo-imaging/handlers/utils/effect/ProcessEffectRequest.ts new file mode 100644 index 0000000..2f3aa52 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/effect/ProcessEffectRequest.ts @@ -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); +} diff --git a/src/app/router/habbo-imaging/handlers/utils/effect/index.ts b/src/app/router/habbo-imaging/handlers/utils/effect/index.ts new file mode 100644 index 0000000..4c36a21 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/effect/index.ts @@ -0,0 +1 @@ +export * from './ProcessEffectRequest'; diff --git a/src/app/router/habbo-imaging/handlers/utils/gesture/ProcessGestureRequest.ts b/src/app/router/habbo-imaging/handlers/utils/gesture/ProcessGestureRequest.ts new file mode 100644 index 0000000..06309ae --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/gesture/ProcessGestureRequest.ts @@ -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; + } +} diff --git a/src/app/router/habbo-imaging/handlers/utils/gesture/index.ts b/src/app/router/habbo-imaging/handlers/utils/gesture/index.ts new file mode 100644 index 0000000..69dea0a --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/gesture/index.ts @@ -0,0 +1 @@ +export * from './ProcessGestureRequest'; diff --git a/src/app/router/habbo-imaging/handlers/utils/index.ts b/src/app/router/habbo-imaging/handlers/utils/index.ts new file mode 100644 index 0000000..b650ac6 --- /dev/null +++ b/src/app/router/habbo-imaging/handlers/utils/index.ts @@ -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'; diff --git a/src/app/router/habbo-imaging/index.ts b/src/app/router/habbo-imaging/index.ts new file mode 100644 index 0000000..cca7206 --- /dev/null +++ b/src/app/router/habbo-imaging/index.ts @@ -0,0 +1 @@ +export * from './HabboImagingRouter'; diff --git a/src/core/NitroCore.ts b/src/core/NitroCore.ts index 369143c..cd7c2e8 100644 --- a/src/core/NitroCore.ts +++ b/src/core/NitroCore.ts @@ -18,9 +18,17 @@ export class NitroCore extends NitroManager implements INitroCore protected async onInit(): Promise { - 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 diff --git a/src/core/asset/AssetManager.ts b/src/core/asset/AssetManager.ts index 192a7b1..2ae7833 100644 --- a/src/core/asset/AssetManager.ts +++ b/src/core/asset/AssetManager.ts @@ -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); diff --git a/src/core/common/NitroManager.ts b/src/core/common/NitroManager.ts index 95ee0fd..26ac232 100644 --- a/src/core/common/NitroManager.ts +++ b/src/core/common/NitroManager.ts @@ -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; diff --git a/src/core/configuration/ConfigurationManager.ts b/src/core/configuration/ConfigurationManager.ts index 760001c..fec94e3 100644 --- a/src/core/configuration/ConfigurationManager.ts +++ b/src/core/configuration/ConfigurationManager.ts @@ -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 { - await this.loadConfigurationFromUrl((process.env.CONFIG_URL || null)); + await this.loadConfigurationFromUrl('./config.json'); } private async loadConfigurationFromUrl(url: string): Promise { - 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'); } diff --git a/src/core/utils/CanvasUtilities.ts b/src/core/utils/CanvasUtilities.ts index 930f869..1d79f05 100644 --- a/src/core/utils/CanvasUtilities.ts +++ b/src/core/utils/CanvasUtilities.ts @@ -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; + } } diff --git a/src/core/utils/Point.ts b/src/core/utils/Point.ts index aa6b7e2..c0d5066 100644 --- a/src/core/utils/Point.ts +++ b/src/core/utils/Point.ts @@ -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; + } } diff --git a/src/main.ts b/src/main.ts index 104b80d..185cbe6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,3 @@ -require('dotenv').config(); - import { Application } from './app'; import { NitroCore } from './core'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7a81293 --- /dev/null +++ b/tsconfig.json @@ -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" + ] +}