2012-06-29 16:44:26 +02:00
//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
2020-07-30 11:42:28 -03:00
// Remere's Map Editor is free software: you can redistribute it and/or modify
2012-06-29 16:44:26 +02:00
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
2016-09-29 19:24:45 -03:00
//
2020-07-30 11:42:28 -03:00
// Remere's Map Editor is distributed in the hope that it will be useful,
2012-06-29 16:44:26 +02:00
// but WITHOUT ANY WARRANTY; without even the implied warranty of
2020-07-30 11:42:28 -03:00
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2012-06-29 16:44:26 +02:00
// GNU General Public License for more details.
2016-09-29 19:24:45 -03:00
//
2012-06-29 16:44:26 +02:00
// You should have received a copy of the GNU General Public License
2020-07-30 11:42:28 -03:00
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2012-06-29 16:44:26 +02:00
//////////////////////////////////////////////////////////////////////
# include "main.h"
# include "graphics.h"
2025-05-10 16:38:45 -03:00
# include "client_assets.h"
2023-10-09 23:02:37 -03:00
# include "artprovider.h"
2012-06-29 16:44:26 +02:00
# include "filehandle.h"
# include "settings.h"
# include "gui.h"
2015-02-17 10:41:13 -03:00
# include "otml.h"
2025-05-10 16:38:45 -03:00
# include "sprite_appearances.h"
# include "sprites.h"
2012-06-29 16:44:26 +02:00
# include "pngfiles.h"
2026-04-22 19:19:35 -03:00
# include "gl_compat.h"
2026-04-17 18:58:28 -03:00
# include "gl_renderer.h"
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
# include <wx/rawbmp.h>
# include <appearances.pb.h>
2026-04-17 18:58:28 -03:00
# ifndef GL_CLAMP_TO_EDGE
feat: add Bitmap to Map terrain generator (#166)
New Features:
- Import Bitmap to Map: new menu entry to launch the conversion workflow.
- Conversion dialog: image preview with zoom, crop, rotate, flip, scale and X/Y/Z offsets; mouse pixel inspection.
- Color detection: auto-detect colors, suggest brushes, per-color ignore/brush selection, adjust tolerance, RGB or hue matching.
- Presets: save/load conversion settings.
- Generate: preview generation, progress feedback, conversion to map tiles with success/error reporting.
2026-04-21 11:55:08 -03:00
# define GL_CLAMP_TO_EDGE 0x812F
2026-04-17 18:58:28 -03:00
# endif
2025-05-10 16:38:45 -03:00
GraphicManager g_graphics ;
GameSprite g_gameSprite ;
2012-06-29 16:44:26 +02:00
// All 133 template colors
static uint32_t TemplateOutfitLookupTable [ ] = {
2023-10-09 19:12:16 -07:00
0xFFFFFF ,
0xFFD4BF ,
0xFFE9BF ,
0xFFFFBF ,
0xE9FFBF ,
0xD4FFBF ,
0xBFFFBF ,
0xBFFFD4 ,
0xBFFFE9 ,
0xBFFFFF ,
0xBFE9FF ,
0xBFD4FF ,
0xBFBFFF ,
0xD4BFFF ,
0xE9BFFF ,
0xFFBFFF ,
0xFFBFE9 ,
0xFFBFD4 ,
0xFFBFBF ,
0xDADADA ,
0xBF9F8F ,
0xBFAF8F ,
0xBFBF8F ,
0xAFBF8F ,
0x9FBF8F ,
0x8FBF8F ,
0x8FBF9F ,
0x8FBFAF ,
0x8FBFBF ,
0x8FAFBF ,
0x8F9FBF ,
0x8F8FBF ,
0x9F8FBF ,
0xAF8FBF ,
0xBF8FBF ,
0xBF8FAF ,
0xBF8F9F ,
0xBF8F8F ,
0xB6B6B6 ,
0xBF7F5F ,
0xBFAF8F ,
0xBFBF5F ,
0x9FBF5F ,
0x7FBF5F ,
0x5FBF5F ,
0x5FBF7F ,
0x5FBF9F ,
0x5FBFBF ,
0x5F9FBF ,
0x5F7FBF ,
0x5F5FBF ,
0x7F5FBF ,
0x9F5FBF ,
0xBF5FBF ,
0xBF5F9F ,
0xBF5F7F ,
0xBF5F5F ,
0x919191 ,
0xBF6A3F ,
0xBF943F ,
0xBFBF3F ,
0x94BF3F ,
0x6ABF3F ,
0x3FBF3F ,
0x3FBF6A ,
0x3FBF94 ,
0x3FBFBF ,
0x3F94BF ,
0x3F6ABF ,
0x3F3FBF ,
0x6A3FBF ,
0x943FBF ,
0xBF3FBF ,
0xBF3F94 ,
0xBF3F6A ,
0xBF3F3F ,
0x6D6D6D ,
0xFF5500 ,
0xFFAA00 ,
0xFFFF00 ,
0xAAFF00 ,
0x54FF00 ,
0x00FF00 ,
0x00FF54 ,
0x00FFAA ,
0x00FFFF ,
0x00A9FF ,
0x0055FF ,
0x0000FF ,
0x5500FF ,
0xA900FF ,
0xFE00FF ,
0xFF00AA ,
0xFF0055 ,
0xFF0000 ,
0x484848 ,
0xBF3F00 ,
0xBF7F00 ,
0xBFBF00 ,
0x7FBF00 ,
0x3FBF00 ,
0x00BF00 ,
0x00BF3F ,
0x00BF7F ,
0x00BFBF ,
0x007FBF ,
0x003FBF ,
0x0000BF ,
0x3F00BF ,
0x7F00BF ,
0xBF00BF ,
0xBF007F ,
0xBF003F ,
0xBF0000 ,
0x242424 ,
0x7F2A00 ,
0x7F5500 ,
0x7F7F00 ,
0x557F00 ,
0x2A7F00 ,
0x007F00 ,
0x007F2A ,
0x007F55 ,
0x007F7F ,
0x00547F ,
0x002A7F ,
0x00007F ,
0x2A007F ,
0x54007F ,
0x7F007F ,
0x7F0055 ,
0x7F002A ,
2012-06-29 16:44:26 +02:00
0x7F0000 ,
} ;
GraphicManager : : GraphicManager ( ) :
2015-02-17 10:41:13 -03:00
otfi_found ( false ) ,
2015-02-05 21:07:44 -03:00
is_extended ( false ) ,
2015-02-17 10:41:13 -03:00
has_frame_durations ( false ) ,
has_frame_groups ( false ) ,
2012-06-29 16:44:26 +02:00
loaded_textures ( 0 ) ,
2026-04-22 19:19:35 -03:00
lastclean { } {
2016-11-09 12:17:17 -03:00
animation_timer = newd wxStopWatch ( ) ;
animation_timer - > Start ( ) ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
GraphicManager : : ~ GraphicManager ( ) {
for ( SpriteMap : : iterator iter = sprite_space . begin ( ) ; iter ! = sprite_space . end ( ) ; + + iter ) {
2012-06-29 16:44:26 +02:00
delete iter - > second ;
2025-05-10 16:38:45 -03:00
iter - > second = nullptr ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
for ( ImageMap : : iterator iter = image_space . begin ( ) ; iter ! = image_space . end ( ) ; + + iter ) {
2012-06-29 16:44:26 +02:00
delete iter - > second ;
2025-05-10 16:38:45 -03:00
iter - > second = nullptr ;
2012-06-29 16:44:26 +02:00
}
2016-11-09 12:17:17 -03:00
2023-10-09 23:02:37 -03:00
sprite_space . clear ( ) ;
image_space . clear ( ) ;
2016-11-09 12:17:17 -03:00
delete animation_timer ;
2025-05-10 16:38:45 -03:00
animation_timer = nullptr ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
GLuint GraphicManager : : getFreeTextureID ( ) {
2026-04-17 18:58:28 -03:00
GLuint id = 0 ;
glGenTextures ( 1 , & id ) ;
return id ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
void GraphicManager : : clear ( ) {
2012-06-29 16:44:26 +02:00
SpriteMap new_sprite_space ;
2023-10-09 19:12:16 -07:00
for ( SpriteMap : : iterator iter = sprite_space . begin ( ) ; iter ! = sprite_space . end ( ) ; + + iter ) {
if ( iter - > first > = 0 ) { // Don't clean internal sprites
2012-06-29 16:44:26 +02:00
delete iter - > second ;
2025-05-10 16:38:45 -03:00
iter - > second = nullptr ;
2012-06-29 16:44:26 +02:00
} else {
new_sprite_space . insert ( std : : make_pair ( iter - > first , iter - > second ) ) ;
}
}
2023-10-09 19:12:16 -07:00
for ( ImageMap : : iterator iter = image_space . begin ( ) ; iter ! = image_space . end ( ) ; + + iter ) {
2012-06-29 16:44:26 +02:00
delete iter - > second ;
2025-05-10 16:38:45 -03:00
iter - > second = nullptr ;
2012-06-29 16:44:26 +02:00
}
sprite_space . swap ( new_sprite_space ) ;
image_space . clear ( ) ;
cleanup_list . clear ( ) ;
item_count = 0 ;
creature_count = 0 ;
loaded_textures = 0 ;
2026-04-22 19:19:35 -03:00
lastclean = std : : chrono : : steady_clock : : now ( ) ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
void GraphicManager : : cleanSoftwareSprites ( ) {
for ( SpriteMap : : iterator iter = sprite_space . begin ( ) ; iter ! = sprite_space . end ( ) ; + + iter ) {
if ( iter - > first > = 0 ) { // Don't clean internal sprites
2012-06-29 16:44:26 +02:00
iter - > second - > unloadDC ( ) ;
}
}
}
2023-10-09 19:12:16 -07:00
Sprite * GraphicManager : : getSprite ( int id ) {
2012-06-29 16:44:26 +02:00
SpriteMap : : iterator it = sprite_space . find ( id ) ;
2023-10-09 19:12:16 -07:00
if ( it ! = sprite_space . end ( ) ) {
2012-06-29 16:44:26 +02:00
return it - > second ;
}
2014-01-22 20:16:17 +01:00
return nullptr ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
GameSprite * GraphicManager : : getCreatureSprite ( int id ) {
if ( id < 0 ) {
2014-01-22 20:16:17 +01:00
return nullptr ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
SpriteMap : : iterator it = sprite_space . find ( id + getItemSpriteMaxID ( ) ) ;
2023-10-09 19:12:16 -07:00
if ( it ! = sprite_space . end ( ) ) {
2012-06-29 16:44:26 +02:00
return static_cast < GameSprite * > ( it - > second ) ;
}
2014-01-22 20:16:17 +01:00
return nullptr ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
uint16_t GraphicManager : : getItemSpriteMaxID ( ) const {
return item_count ;
}
uint16_t GraphicManager : : getCreatureSpriteMaxID ( ) const {
return creature_count ;
}
2023-10-09 19:12:16 -07:00
GameSprite * GraphicManager : : getEditorSprite ( int id ) {
if ( id > = 0 ) {
2023-10-09 23:02:37 -03:00
return nullptr ;
}
2012-06-29 16:44:26 +02:00
2023-10-09 23:02:37 -03:00
SpriteMap : : iterator it = sprite_space . find ( id ) ;
2023-10-09 19:12:16 -07:00
if ( it ! = sprite_space . end ( ) ) {
2023-10-09 23:02:37 -03:00
return dynamic_cast < GameSprite * > ( it - > second ) ;
}
return nullptr ;
2012-06-29 16:44:26 +02:00
}
# define loadPNGFile(name) _wxGetBitmapFromMemory(name, sizeof(name))
2023-10-09 19:12:16 -07:00
inline wxBitmap * _wxGetBitmapFromMemory ( const unsigned char * data , int length ) {
2012-06-29 16:44:26 +02:00
wxMemoryInputStream is ( data , length ) ;
2016-11-05 15:28:14 +01:00
wxImage img ( is , " image/png " ) ;
2023-10-09 19:12:16 -07:00
if ( ! img . IsOk ( ) ) {
return nullptr ;
}
2012-06-29 16:44:26 +02:00
return newd wxBitmap ( img , - 1 ) ;
}
2023-10-09 19:12:16 -07:00
bool GraphicManager : : loadEditorSprites ( ) {
2012-06-29 16:44:26 +02:00
// Unused graphics MIGHT be loaded here, but it's a neglectable loss
2023-10-09 19:12:16 -07:00
sprite_space [ EDITOR_SPRITE_SELECTION_MARKER ] = newd EditorSprite (
newd wxBitmap ( selection_marker_xpm16x16 ) ,
newd wxBitmap ( selection_marker_xpm32x32 )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_1x1 ] = newd EditorSprite (
loadPNGFile ( circular_1_small_png ) ,
loadPNGFile ( circular_1_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_3x3 ] = newd EditorSprite (
loadPNGFile ( circular_2_small_png ) ,
loadPNGFile ( circular_2_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_5x5 ] = newd EditorSprite (
loadPNGFile ( circular_3_small_png ) ,
loadPNGFile ( circular_3_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_7x7 ] = newd EditorSprite (
loadPNGFile ( circular_4_small_png ) ,
loadPNGFile ( circular_4_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_9x9 ] = newd EditorSprite (
loadPNGFile ( circular_5_small_png ) ,
loadPNGFile ( circular_5_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_15x15 ] = newd EditorSprite (
loadPNGFile ( circular_6_small_png ) ,
loadPNGFile ( circular_6_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_CD_19x19 ] = newd EditorSprite (
loadPNGFile ( circular_7_small_png ) ,
loadPNGFile ( circular_7_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_1x1 ] = newd EditorSprite (
loadPNGFile ( rectangular_1_small_png ) ,
loadPNGFile ( rectangular_1_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_3x3 ] = newd EditorSprite (
loadPNGFile ( rectangular_2_small_png ) ,
loadPNGFile ( rectangular_2_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_5x5 ] = newd EditorSprite (
loadPNGFile ( rectangular_3_small_png ) ,
loadPNGFile ( rectangular_3_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_7x7 ] = newd EditorSprite (
loadPNGFile ( rectangular_4_small_png ) ,
loadPNGFile ( rectangular_4_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_9x9 ] = newd EditorSprite (
loadPNGFile ( rectangular_5_small_png ) ,
loadPNGFile ( rectangular_5_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_15x15 ] = newd EditorSprite (
loadPNGFile ( rectangular_6_small_png ) ,
loadPNGFile ( rectangular_6_png )
) ;
sprite_space [ EDITOR_SPRITE_BRUSH_SD_19x19 ] = newd EditorSprite (
loadPNGFile ( rectangular_7_small_png ) ,
loadPNGFile ( rectangular_7_png )
) ;
sprite_space [ EDITOR_SPRITE_OPTIONAL_BORDER_TOOL ] = newd EditorSprite (
loadPNGFile ( optional_border_small_png ) ,
loadPNGFile ( optional_border_png )
) ;
sprite_space [ EDITOR_SPRITE_ERASER ] = newd EditorSprite (
loadPNGFile ( eraser_small_png ) ,
loadPNGFile ( eraser_png )
) ;
sprite_space [ EDITOR_SPRITE_PZ_TOOL ] = newd EditorSprite (
loadPNGFile ( protection_zone_small_png ) ,
loadPNGFile ( protection_zone_png )
) ;
sprite_space [ EDITOR_SPRITE_PVPZ_TOOL ] = newd EditorSprite (
loadPNGFile ( pvp_zone_small_png ) ,
loadPNGFile ( pvp_zone_png )
) ;
sprite_space [ EDITOR_SPRITE_NOLOG_TOOL ] = newd EditorSprite (
loadPNGFile ( no_logout_small_png ) ,
loadPNGFile ( no_logout_png )
) ;
sprite_space [ EDITOR_SPRITE_NOPVP_TOOL ] = newd EditorSprite (
loadPNGFile ( no_pvp_small_png ) ,
loadPNGFile ( no_pvp_png )
) ;
sprite_space [ EDITOR_SPRITE_DOOR_NORMAL ] = newd EditorSprite (
loadPNGFile ( door_normal_small_png ) ,
loadPNGFile ( door_normal_png )
) ;
sprite_space [ EDITOR_SPRITE_DOOR_LOCKED ] = newd EditorSprite (
loadPNGFile ( door_locked_small_png ) ,
loadPNGFile ( door_locked_png )
) ;
sprite_space [ EDITOR_SPRITE_DOOR_MAGIC ] = newd EditorSprite (
loadPNGFile ( door_magic_small_png ) ,
loadPNGFile ( door_magic_png )
) ;
sprite_space [ EDITOR_SPRITE_DOOR_QUEST ] = newd EditorSprite (
loadPNGFile ( door_quest_small_png ) ,
loadPNGFile ( door_quest_png )
) ;
sprite_space [ EDITOR_SPRITE_WINDOW_NORMAL ] = newd EditorSprite (
loadPNGFile ( window_normal_small_png ) ,
loadPNGFile ( window_normal_png )
) ;
sprite_space [ EDITOR_SPRITE_WINDOW_HATCH ] = newd EditorSprite (
loadPNGFile ( window_hatch_small_png ) ,
loadPNGFile ( window_hatch_png )
) ;
sprite_space [ EDITOR_SPRITE_SELECTION_GEM ] = newd EditorSprite (
loadPNGFile ( gem_edit_png ) ,
nullptr
) ;
sprite_space [ EDITOR_SPRITE_DRAWING_GEM ] = newd EditorSprite (
loadPNGFile ( gem_move_png ) ,
nullptr
) ;
2012-06-29 16:44:26 +02:00
2023-10-27 15:52:05 -03:00
sprite_space [ EDITOR_SPRITE_MONSTERS ] = GameSprite : : createFromBitmap ( ART_MONSTERS ) ;
sprite_space [ EDITOR_SPRITE_NPCS ] = GameSprite : : createFromBitmap ( ART_NPCS ) ;
2023-10-09 23:02:37 -03:00
sprite_space [ EDITOR_SPRITE_HOUSE_EXIT ] = GameSprite : : createFromBitmap ( ART_HOUSE_EXIT ) ;
sprite_space [ EDITOR_SPRITE_PICKUPABLE_ITEM ] = GameSprite : : createFromBitmap ( ART_PICKUPABLE ) ;
sprite_space [ EDITOR_SPRITE_MOVEABLE_ITEM ] = GameSprite : : createFromBitmap ( ART_MOVEABLE ) ;
sprite_space [ EDITOR_SPRITE_PICKUPABLE_MOVEABLE_ITEM ] = GameSprite : : createFromBitmap ( ART_PICKUPABLE_MOVEABLE ) ;
2024-08-14 16:54:14 -03:00
sprite_space [ EDITOR_SPRITE_AVOIDABLE_ITEM ] = GameSprite : : createFromBitmap ( ART_AVOIDABLE ) ;
2023-10-09 23:02:37 -03:00
2012-06-29 16:44:26 +02:00
return true ;
}
2025-05-10 16:38:45 -03:00
# if CLIENT_VERSION < 1100
2023-10-09 19:12:16 -07:00
bool GraphicManager : : loadOTFI ( const FileName & filename , wxString & error , wxArrayString & warnings ) {
2019-07-23 10:21:07 -03:00
wxDir dir ( filename . GetFullPath ( ) ) ;
wxString otfi_file ;
2015-02-17 10:41:13 -03:00
2019-07-23 10:21:07 -03:00
otfi_found = false ;
2023-10-09 19:12:16 -07:00
if ( dir . GetFirst ( & otfi_file , " *.otfi " , wxDIR_FILES ) ) {
2019-07-23 10:21:07 -03:00
wxFileName otfi ( filename . GetFullPath ( ) , otfi_file ) ;
OTMLDocumentPtr doc = OTMLDocument : : parse ( otfi . GetFullPath ( ) . ToStdString ( ) ) ;
2023-10-09 19:12:16 -07:00
if ( doc - > size ( ) = = 0 | | ! doc - > hasChildAt ( " DatSpr " ) ) {
2016-11-05 15:28:14 +01:00
error + = " 'DatSpr' tag not found " ;
2015-02-17 10:41:13 -03:00
return false ;
}
OTMLNodePtr node = doc - > get ( " DatSpr " ) ;
is_extended = node - > valueAt < bool > ( " extended " ) ;
has_transparency = node - > valueAt < bool > ( " transparency " ) ;
has_frame_durations = node - > valueAt < bool > ( " frame-durations " ) ;
has_frame_groups = node - > valueAt < bool > ( " frame-groups " ) ;
2019-07-23 10:21:07 -03:00
std : : string metadata = node - > valueAt < std : : string > ( " metadata-file " , std : : string ( ASSETS_NAME ) + " .dat " ) ;
std : : string sprites = node - > valueAt < std : : string > ( " sprites-file " , std : : string ( ASSETS_NAME ) + " .spr " ) ;
metadata_file = wxFileName ( filename . GetFullPath ( ) , wxString ( metadata ) ) ;
sprites_file = wxFileName ( filename . GetFullPath ( ) , wxString ( sprites ) ) ;
otfi_found = true ;
}
2020-07-30 11:42:28 -03:00
2023-10-09 19:12:16 -07:00
if ( ! otfi_found ) {
2015-02-17 10:41:13 -03:00
is_extended = false ;
has_transparency = false ;
has_frame_durations = false ;
has_frame_groups = false ;
2019-07-23 10:21:07 -03:00
metadata_file = wxFileName ( filename . GetFullPath ( ) , wxString ( ASSETS_NAME ) + " .dat " ) ;
sprites_file = wxFileName ( filename . GetFullPath ( ) , wxString ( ASSETS_NAME ) + " .spr " ) ;
2015-02-17 10:41:13 -03:00
}
2019-07-23 10:21:07 -03:00
2015-02-17 10:41:13 -03:00
return true ;
}
2023-10-09 19:12:16 -07:00
bool GraphicManager : : loadSpriteMetadata ( const FileName & datafile , wxString & error , wxArrayString & warnings ) {
2012-06-29 16:44:26 +02:00
// items.otb has most of the info we need. This only loads the GameSprite metadata
FileReadHandle file ( nstr ( datafile . GetFullPath ( ) ) ) ;
2023-10-09 19:12:16 -07:00
if ( ! file . isOk ( ) ) {
2016-11-05 15:28:14 +01:00
error + = " Failed to open " + datafile . GetFullPath ( ) + " for reading \n The error reported was: " + wxstr ( file . getErrorMessage ( ) ) ;
2012-06-29 16:44:26 +02:00
return false ;
}
uint16_t effect_count , distance_count ;
2012-07-01 16:35:50 +02:00
uint32_t datSignature ;
file . getU32 ( datSignature ) ;
2023-10-09 19:12:16 -07:00
// get max id
2012-06-29 16:44:26 +02:00
file . getU16 ( item_count ) ;
file . getU16 ( creature_count ) ;
file . getU16 ( effect_count ) ;
file . getU16 ( distance_count ) ;
2018-10-14 19:54:08 -03:00
uint32_t minID = 100 ; // items start with id 100
2012-06-29 16:44:26 +02:00
// We don't load distance/effects, if we would, just add effect_count & distance_count here
2017-05-19 15:19:10 -03:00
uint32_t maxID = item_count + creature_count ;
2012-06-29 16:44:26 +02:00
2015-02-05 21:07:44 -03:00
dat_format = client_version - > getDatFormatForSignature ( datSignature ) ;
2015-02-17 10:41:13 -03:00
2023-10-09 19:12:16 -07:00
if ( dat_format = = DAT_FORMAT_UNKNOWN ) {
2022-03-03 13:41:11 -08:00
error + = " Failed to open " + datafile . GetFullPath ( ) + " for reading \n Could not locate datSignature (0x " < < wxString : : Format ( wxT ( " %02x " ) , datSignature ) < < " ) compatible with client version " < < client_version - > getName ( ) ;
return false ;
}
2023-10-09 19:12:16 -07:00
if ( ! otfi_found ) {
2021-06-23 21:30:37 -03:00
is_extended = dat_format > = DAT_FORMAT_11 ;
has_frame_durations = dat_format > = DAT_FORMAT_11 ;
has_frame_groups = dat_format > = DAT_FORMAT_11 ;
2015-02-17 10:41:13 -03:00
}
2015-02-05 21:07:44 -03:00
2017-05-19 15:19:10 -03:00
uint16_t id = minID ;
2012-06-29 16:44:26 +02:00
// loop through all ItemDatabase until we reach the end of file
2023-10-09 19:12:16 -07:00
while ( id < = maxID ) {
2012-06-29 16:44:26 +02:00
GameSprite * sType = newd GameSprite ( ) ;
sprite_space [ id ] = sType ;
sType - > id = id ;
2012-07-01 15:55:44 +02:00
// Load the sprite flags
2023-10-09 19:12:16 -07:00
if ( ! loadSpriteMetadataFlags ( file , sType , error , warnings ) ) {
2012-07-01 15:55:44 +02:00
wxString msg ;
2016-11-05 15:28:14 +01:00
msg < < " Failed to load flags for sprite " < < sType - > id ;
2012-07-01 15:55:44 +02:00
warnings . push_back ( msg ) ;
}
2012-06-29 16:44:26 +02:00
2015-02-01 13:42:24 -03:00
// Reads the group count
2015-02-17 10:41:13 -03:00
uint8_t group_count = 1 ;
2023-10-09 19:12:16 -07:00
if ( has_frame_groups & & id > item_count ) {
2015-02-03 22:59:14 -03:00
file . getU8 ( group_count ) ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
for ( uint32_t k = 0 ; k < group_count ; + + k ) {
2015-02-01 13:42:24 -03:00
// Skipping the group type
2023-10-09 19:12:16 -07:00
if ( has_frame_groups & & id > item_count ) {
2015-02-01 13:42:24 -03:00
file . skip ( 1 ) ;
}
// Size and GameSprite data
file . getByte ( sType - > width ) ;
file . getByte ( sType - > height ) ;
// Skipping the exact size
2023-10-09 19:12:16 -07:00
if ( ( sType - > width > 1 ) | | ( sType - > height > 1 ) ) {
2015-02-01 13:42:24 -03:00
file . skip ( 1 ) ;
2012-07-01 16:49:44 +02:00
}
2015-02-01 13:42:24 -03:00
2015-02-03 22:59:14 -03:00
file . getU8 ( sType - > layers ) ; // Number of blendframes (some sprites consist of several merged sprites)
file . getU8 ( sType - > pattern_x ) ;
file . getU8 ( sType - > pattern_y ) ;
2021-06-23 21:30:37 -03:00
file . getU8 ( sType - > pattern_z ) ;
2015-02-03 22:59:14 -03:00
file . getU8 ( sType - > frames ) ; // Length of animation
2020-07-30 11:42:28 -03:00
2023-10-09 19:12:16 -07:00
if ( sType - > frames > 1 ) {
2016-11-09 12:17:17 -03:00
uint8_t async = 0 ;
int loop_count = 0 ;
int8_t start_frame = 0 ;
2023-10-09 19:12:16 -07:00
if ( has_frame_durations ) {
2016-11-09 12:17:17 -03:00
file . getByte ( async ) ;
file . get32 ( loop_count ) ;
file . getSByte ( start_frame ) ;
}
sType - > animator = newd Animator ( sType - > frames , start_frame , loop_count , async = = 1 ) ;
2023-10-09 19:12:16 -07:00
if ( has_frame_durations ) {
for ( int i = 0 ; i < sType - > frames ; i + + ) {
2016-11-09 12:17:17 -03:00
uint32_t min ;
uint32_t max ;
file . getU32 ( min ) ;
file . getU32 ( max ) ;
FrameDuration * frame_duration = sType - > animator - > getFrameDuration ( i ) ;
frame_duration - > setValues ( int ( min ) , int ( max ) ) ;
}
sType - > animator - > reset ( ) ;
}
2012-07-01 16:35:50 +02:00
}
2015-02-01 13:42:24 -03:00
2023-10-09 19:12:16 -07:00
sType - > numsprites = ( int ) sType - > width * ( int ) sType - > height * ( int ) sType - > layers * ( int ) sType - > pattern_x * ( int ) sType - > pattern_y * sType - > pattern_z * ( int ) sType - > frames ;
2015-02-01 13:42:24 -03:00
// Read the sprite ids
2023-10-09 19:12:16 -07:00
for ( uint32_t i = 0 ; i < sType - > numsprites ; + + i ) {
2015-02-01 13:42:24 -03:00
uint32_t sprite_id ;
2023-10-09 19:12:16 -07:00
if ( is_extended ) {
2015-02-01 13:42:24 -03:00
file . getU32 ( sprite_id ) ;
2015-02-05 21:07:44 -03:00
} else {
2015-02-01 13:42:24 -03:00
uint16_t u16 = 0 ;
file . getU16 ( u16 ) ;
sprite_id = u16 ;
}
2023-10-09 19:12:16 -07:00
if ( image_space [ sprite_id ] = = nullptr ) {
2015-02-01 13:42:24 -03:00
GameSprite : : NormalImage * img = newd GameSprite : : NormalImage ( ) ;
img - > id = sprite_id ;
image_space [ sprite_id ] = img ;
}
sType - > spriteList . push_back ( static_cast < GameSprite : : NormalImage * > ( image_space [ sprite_id ] ) ) ;
2012-06-29 16:44:26 +02:00
}
}
+ + id ;
}
return true ;
}
2023-10-09 19:12:16 -07:00
bool GraphicManager : : loadSpriteMetadataFlags ( FileReadHandle & file , GameSprite * sType , wxString & error , wxArrayString & warnings ) {
2015-02-06 10:30:02 -03:00
uint8_t prev_flag = 0 ;
uint8_t flag = DatFlagLast ;
2012-07-01 15:55:44 +02:00
2023-10-09 19:12:16 -07:00
for ( int i = 0 ; i < DatFlagLast ; + + i ) {
2015-02-06 10:30:02 -03:00
prev_flag = flag ;
file . getU8 ( flag ) ;
2012-07-01 15:55:44 +02:00
2023-10-09 19:12:16 -07:00
if ( flag = = DatFlagLast ) {
2015-02-06 10:30:02 -03:00
return true ;
}
2023-10-09 19:12:16 -07:00
if ( dat_format > = DAT_FORMAT_11 ) {
2015-02-06 10:30:02 -03:00
/* In 10.10+ all attributes from 16 and up were
2023-10-09 19:12:16 -07:00
* incremented by 1 to make space for 16 as
* " No Movement Animation " flag .
*/
if ( flag = = 16 ) {
2015-02-06 10:30:02 -03:00
flag = DatFlagNoMoveAnimation ;
2023-10-09 19:12:16 -07:00
} else if ( flag > 16 ) {
2015-02-06 10:30:02 -03:00
flag - = 1 ;
2023-10-09 19:12:16 -07:00
}
2015-02-06 10:30:02 -03:00
}
2012-07-01 15:55:44 +02:00
2015-12-06 11:42:37 -03:00
switch ( flag ) {
2015-02-06 10:30:02 -03:00
case DatFlagGroundBorder :
case DatFlagOnBottom :
case DatFlagOnTop :
case DatFlagContainer :
case DatFlagStackable :
case DatFlagForceUse :
case DatFlagMultiUse :
case DatFlagFluidContainer :
case DatFlagSplash :
case DatFlagNotWalkable :
case DatFlagNotMoveable :
case DatFlagBlockProjectile :
case DatFlagNotPathable :
case DatFlagPickupable :
case DatFlagHangable :
case DatFlagHookSouth :
case DatFlagHookEast :
case DatFlagRotateable :
case DatFlagDontHide :
case DatFlagTranslucent :
case DatFlagLyingCorpse :
case DatFlagAnimateAlways :
case DatFlagFullGround :
case DatFlagLook :
2016-06-10 14:40:02 -03:00
case DatFlagWrappable :
case DatFlagUnwrappable :
case DatFlagTopEffect :
2015-02-06 10:30:02 -03:00
case DatFlagFloorChange :
case DatFlagNoMoveAnimation :
case DatFlagChargeable :
break ;
case DatFlagWritable :
case DatFlagWritableOnce :
case DatFlagCloth :
case DatFlagLensHelp :
case DatFlagUsable :
file . skip ( 2 ) ;
break ;
2023-10-09 23:02:37 -03:00
case DatFlagGround :
uint16_t speed ;
file . getU16 ( speed ) ;
2023-10-09 19:12:16 -07:00
sType - > ground_speed = speed ;
2012-07-01 15:55:44 +02:00
break ;
2015-02-06 10:30:02 -03:00
2023-10-09 23:02:37 -03:00
case DatFlagLight : {
SpriteLight light ;
uint16_t intensity ;
uint16_t color ;
file . getU16 ( intensity ) ;
file . getU16 ( color ) ;
sType - > has_light = true ;
2023-10-09 19:12:16 -07:00
sType - > light = SpriteLight { static_cast < uint8_t > ( intensity ) , static_cast < uint8_t > ( color ) } ;
2023-10-09 23:02:37 -03:00
break ;
}
2015-12-06 11:42:37 -03:00
case DatFlagDisplacement : {
2023-10-09 19:12:16 -07:00
if ( dat_format > = DAT_FORMAT_11 ) {
2015-02-06 10:30:02 -03:00
uint16_t offset_x ;
uint16_t offset_y ;
file . getU16 ( offset_x ) ;
file . getU16 ( offset_y ) ;
2023-10-09 23:02:37 -03:00
sType - > draw_offset = wxPoint ( offset_x , offset_y ) ;
} else {
sType - > draw_offset = wxPoint ( 8 , 8 ) ;
2015-02-06 10:30:02 -03:00
}
2012-07-01 15:55:44 +02:00
break ;
}
2015-02-06 10:30:02 -03:00
2015-12-06 11:42:37 -03:00
case DatFlagElevation : {
2015-02-06 10:30:02 -03:00
uint16_t draw_height ;
file . getU16 ( draw_height ) ;
sType - > draw_height = draw_height ;
2012-07-01 15:55:44 +02:00
break ;
}
2015-02-06 10:30:02 -03:00
2015-12-06 11:42:37 -03:00
case DatFlagMinimapColor : {
2015-02-06 10:30:02 -03:00
uint16_t minimap_color ;
file . getU16 ( minimap_color ) ;
sType - > minimap_color = minimap_color ;
2012-07-01 15:55:44 +02:00
break ;
}
2015-02-06 10:30:02 -03:00
2015-12-06 11:42:37 -03:00
case DatFlagMarket : {
2015-02-06 10:30:02 -03:00
file . skip ( 6 ) ;
std : : string marketName ;
file . getString ( marketName ) ;
file . skip ( 4 ) ;
2012-07-01 15:55:44 +02:00
break ;
}
2015-02-06 10:30:02 -03:00
2015-12-06 11:42:37 -03:00
default : {
2012-07-01 15:55:44 +02:00
wxString err ;
2016-11-05 15:28:14 +01:00
err < < " Metadata: Unknown flag: " < < i2ws ( flag ) < < " . Previous flag: " < < i2ws ( prev_flag ) < < " . " ;
2012-07-01 15:55:44 +02:00
warnings . push_back ( err ) ;
break ;
2012-07-01 16:35:50 +02:00
}
2012-07-01 15:55:44 +02:00
}
2014-02-21 13:55:09 +01:00
}
2015-02-06 10:30:02 -03:00
return true ;
2014-02-21 13:55:09 +01:00
}
2023-10-09 19:12:16 -07:00
bool GraphicManager : : loadSpriteData ( const FileName & datafile , wxString & error , wxArrayString & warnings ) {
2012-06-29 16:44:26 +02:00
FileReadHandle fh ( nstr ( datafile . GetFullPath ( ) ) ) ;
2023-10-09 19:12:16 -07:00
if ( ! fh . isOk ( ) ) {
2016-11-05 15:28:14 +01:00
error = " Failed to open file for reading " ;
2012-06-29 16:44:26 +02:00
return false ;
}
2025-05-10 16:38:45 -03:00
# define safe_get(func, ...) \
do { \
if ( ! fh . get # # func ( __VA_ARGS__ ) ) { \
error = wxstr ( fh . getErrorMessage ( ) ) ; \
return false ; \
} \
} while ( false )
2016-09-29 19:24:45 -03:00
2012-07-01 16:35:50 +02:00
uint32_t sprSignature ;
safe_get ( U32 , sprSignature ) ;
2015-01-23 10:30:09 -03:00
2015-02-05 21:07:44 -03:00
uint32_t total_pics = 0 ;
2023-10-09 19:12:16 -07:00
if ( is_extended ) {
2015-02-05 21:07:44 -03:00
safe_get ( U32 , total_pics ) ;
} else {
2015-01-23 10:30:09 -03:00
uint16_t u16 = 0 ;
safe_get ( U16 , u16 ) ;
total_pics = u16 ;
}
2012-06-29 16:44:26 +02:00
2023-10-09 19:12:16 -07:00
if ( ! g_settings . getInteger ( Config : : USE_MEMCACHED_SPRITES ) ) {
2012-06-29 16:44:26 +02:00
spritefile = nstr ( datafile . GetFullPath ( ) ) ;
unloaded = false ;
return true ;
}
2014-02-28 22:34:25 +01:00
std : : vector < uint32_t > sprite_indexes ;
2023-10-09 19:12:16 -07:00
for ( uint32_t i = 0 ; i < total_pics ; + + i ) {
2012-06-29 16:44:26 +02:00
uint32_t index ;
safe_get ( U32 , index ) ;
sprite_indexes . push_back ( index ) ;
}
2012-07-01 16:35:50 +02:00
// Now read individual sprites
2012-06-29 16:44:26 +02:00
int id = 1 ;
2023-10-09 19:12:16 -07:00
for ( std : : vector < uint32_t > : : iterator sprite_iter = sprite_indexes . begin ( ) ; sprite_iter ! = sprite_indexes . end ( ) ; + + sprite_iter , + + id ) {
2014-02-28 22:34:25 +01:00
uint32_t index = * sprite_iter + 3 ;
2012-06-29 16:44:26 +02:00
fh . seek ( index ) ;
uint16_t size ;
safe_get ( U16 , size ) ;
ImageMap : : iterator it = image_space . find ( id ) ;
2023-10-09 19:12:16 -07:00
if ( it ! = image_space . end ( ) ) {
2012-06-29 16:44:26 +02:00
GameSprite : : NormalImage * spr = dynamic_cast < GameSprite : : NormalImage * > ( it - > second ) ;
2023-10-09 19:12:16 -07:00
if ( spr & & size > 0 ) {
if ( spr - > size > 0 ) {
2012-06-29 16:44:26 +02:00
wxString ss ;
2016-11-05 15:28:14 +01:00
ss < < " items.spr: Duplicate GameSprite id " < < id ;
2012-06-29 16:44:26 +02:00
warnings . push_back ( ss ) ;
2023-10-09 23:02:37 -03:00
fh . skip ( size ) ;
2012-06-29 16:44:26 +02:00
} else {
spr - > id = id ;
spr - > size = size ;
2025-05-10 16:38:45 -03:00
spr - > m_cachedData = newd uint8_t [ size ] ;
if ( ! fh . getRAW ( spr - > m_cachedData , size ) ) {
2023-10-09 19:12:16 -07:00
error = wxstr ( fh . getErrorMessage ( ) ) ;
2012-06-29 16:44:26 +02:00
return false ;
}
}
}
} else {
2023-10-09 23:02:37 -03:00
fh . skip ( size ) ;
2012-06-29 16:44:26 +02:00
}
}
2025-05-10 16:38:45 -03:00
# undef safe_get
2012-06-29 16:44:26 +02:00
unloaded = false ;
return true ;
}
2023-10-09 19:12:16 -07:00
bool GraphicManager : : loadSpriteDump ( uint8_t * & target , uint16_t & size , int sprite_id ) {
if ( g_settings . getInteger ( Config : : USE_MEMCACHED_SPRITES ) ) {
2012-06-29 16:44:26 +02:00
return false ;
2023-10-09 19:12:16 -07:00
}
2012-07-01 16:35:50 +02:00
2023-10-09 19:12:16 -07:00
if ( sprite_id = = 0 ) {
2012-06-29 16:44:26 +02:00
// Empty GameSprite
size = 0 ;
2014-01-22 20:16:17 +01:00
target = nullptr ;
2012-06-29 16:44:26 +02:00
return true ;
}
FileReadHandle fh ( spritefile ) ;
2023-10-09 19:12:16 -07:00
if ( ! fh . isOk ( ) ) {
2012-06-29 16:44:26 +02:00
return false ;
2023-10-09 19:12:16 -07:00
}
2012-06-29 16:44:26 +02:00
unloaded = false ;
2012-07-01 16:35:50 +02:00
2023-10-09 19:12:16 -07:00
if ( ! fh . seek ( ( is_extended ? 4 : 2 ) + sprite_id * sizeof ( uint32_t ) ) ) {
2015-01-23 10:30:09 -03:00
return false ;
2023-10-09 19:12:16 -07:00
}
2012-06-29 16:44:26 +02:00
uint32_t to_seek = 0 ;
2023-10-09 19:12:16 -07:00
if ( fh . getU32 ( to_seek ) ) {
fh . seek ( to_seek + 3 ) ;
2012-06-29 16:44:26 +02:00
uint16_t sprite_size ;
2023-10-09 19:12:16 -07:00
if ( fh . getU16 ( sprite_size ) ) {
2012-06-29 16:44:26 +02:00
target = newd uint8_t [ sprite_size ] ;
2023-10-09 19:12:16 -07:00
if ( fh . getRAW ( target , sprite_size ) ) {
2012-06-29 16:44:26 +02:00
size = sprite_size ;
return true ;
}
delete [ ] target ;
2014-01-22 20:16:17 +01:00
target = nullptr ;
2012-06-29 16:44:26 +02:00
}
}
return false ;
}
2025-05-10 16:38:45 -03:00
# endif
bool GraphicManager : : loadItemSpriteMetadata ( const std : : shared_ptr < ItemType > & t , wxString & error , wxArrayString & warnings ) {
GameSprite * sType = newd GameSprite ( ) ;
sType - > id = t - > id ;
sprite_space [ t - > id ] = sType ;
item_count = std : : max < uint16_t > ( item_count , t - > id ) ;
// Number of blendframes (some sprites consist of several merged sprites
sType - > layers = t - > layers ;
sType - > pattern_x = t - > pattern_width ;
sType - > pattern_y = t - > pattern_height ;
sType - > pattern_z = t - > pattern_depth ;
// Length of animation
sType - > sprite_phase_size = t - > m_animationPhases . size ( ) ;
has_frame_durations = t - > m_animationPhases . size ( ) > 0 ;
if ( sType - > sprite_phase_size > 0 ) {
2026-04-14 13:39:40 -03:00
sType - > animator = newd Animator ( sType - > sprite_phase_size , static_cast < int > ( t - > start_frame ) , t - > loop_count , t - > async_animation ) ;
2025-05-10 16:38:45 -03:00
if ( has_frame_durations ) {
int frameIndex = 0 ;
for ( const auto phase : t - > m_animationPhases ) {
FrameDuration * frame_duration = sType - > animator - > getFrameDuration ( frameIndex ) ;
frame_duration - > setValues ( phase . first , phase . second ) ;
frameIndex + + ;
}
sType - > animator - > reset ( ) ;
}
}
sType - > numsprites = ( int ) sType - > layers * ( int ) sType - > pattern_x * ( int ) sType - > pattern_y * sType - > pattern_z * std : : max < int > ( 1 , sType - > sprite_phase_size ) ;
// Read the sprite ids
for ( uint32_t i = 0 ; i < sType - > numsprites ; + + i ) {
uint32_t sprite_id = t - > m_sprites [ i ] ;
if ( image_space [ sprite_id ] = = nullptr ) {
GameSprite : : NormalImage * img = newd GameSprite : : NormalImage ( ) ;
img - > id = sprite_id ;
image_space [ sprite_id ] = img ;
}
sType - > spriteList . push_back ( static_cast < GameSprite : : NormalImage * > ( image_space [ sprite_id ] ) ) ;
}
return true ;
}
bool GraphicManager : : loadOutfitSpriteMetadata ( canary : : protobuf : : appearances : : Appearance outfit , wxString & error , wxArrayString & warnings ) {
GameSprite * sType = newd GameSprite ( ) ;
sType - > id = outfit . id ( ) + getItemSpriteMaxID ( ) ;
sprite_space [ outfit . id ( ) + getItemSpriteMaxID ( ) ] = sType ;
creature_count = std : : max < uint16_t > ( creature_count , outfit . id ( ) ) ;
// We dont need to worry about IDLE or MOVING frame group
const auto & frameGroup = outfit . frame_group ( ) . Get ( 0 ) ;
const auto & spriteInfo = frameGroup . sprite_info ( ) ;
const auto & animation = spriteInfo . animation ( ) ;
// Number of blendframes (some sprites consist of several merged sprites
sType - > layers = spriteInfo . layers ( ) ;
sType - > pattern_x = spriteInfo . pattern_width ( ) ;
sType - > pattern_y = spriteInfo . pattern_height ( ) ;
sType - > pattern_z = spriteInfo . pattern_depth ( ) ;
// Length of animation
sType - > sprite_phase_size = animation . sprite_phase ( ) . size ( ) ;
has_frame_durations = animation . sprite_phase ( ) . size ( ) > 0 ;
if ( sType - > sprite_phase_size > 0 ) {
2026-04-14 13:39:40 -03:00
sType - > animator = newd Animator ( sType - > sprite_phase_size , static_cast < int8_t > ( animation . default_start_phase ( ) ) , animation . loop_count ( ) , ! animation . synchronized ( ) ) ;
2025-05-10 16:38:45 -03:00
if ( has_frame_durations ) {
int frameIndex = 0 ;
for ( const auto & phase : animation . sprite_phase ( ) ) {
FrameDuration * frame_duration = sType - > animator - > getFrameDuration ( frameIndex ) ;
frame_duration - > setValues ( phase . duration_min ( ) , phase . duration_max ( ) ) ;
frameIndex + + ;
}
sType - > animator - > reset ( ) ;
}
}
sType - > numsprites = ( int ) sType - > layers * ( int ) sType - > pattern_x * ( int ) sType - > pattern_y * sType - > pattern_z * std : : max < int > ( 1 , sType - > sprite_phase_size ) ;
sType - > minimap_color = outfit . flags ( ) . has_automap ( ) ? static_cast < uint16_t > ( outfit . flags ( ) . automap ( ) . color ( ) ) : 0 ;
sType - > draw_height = outfit . flags ( ) . has_height ( ) ? static_cast < uint16_t > ( outfit . flags ( ) . height ( ) . elevation ( ) ) : 0 ;
if ( outfit . flags ( ) . has_shift ( ) ) {
wxPoint drawOffset ( 0 , 0 ) ;
if ( sType - > width > 32 | | sType - > height > 32 ) {
drawOffset = sType - > getDrawOffset ( ) ;
}
drawOffset . x + = outfit . flags ( ) . shift ( ) . x ( ) ;
drawOffset . y + = outfit . flags ( ) . shift ( ) . y ( ) ;
sType - > draw_offset = drawOffset ;
}
// Read the sprite ids
for ( uint32_t i = 0 ; i < sType - > numsprites ; + + i ) {
uint32_t sprite_id = spriteInfo . sprite_id ( ) . Get ( i ) ;
if ( image_space [ sprite_id ] = = nullptr ) {
GameSprite : : NormalImage * img = newd GameSprite : : NormalImage ( ) ;
img - > id = sprite_id ;
image_space [ sprite_id ] = img ;
}
sType - > spriteList . push_back ( static_cast < GameSprite : : NormalImage * > ( image_space [ sprite_id ] ) ) ;
}
return true ;
}
bool GraphicManager : : loadSpriteDump ( uint8_t * & target , uint16_t & size , int sprite_id ) {
// Empty GameSprite
if ( sprite_id = = 0 ) {
size = 0 ;
target = nullptr ;
return true ;
}
const auto & spritePtr = g_spriteAppearances . getSprite ( sprite_id ) ;
if ( ! spritePtr ) {
return false ;
}
size = spritePtr - > pixels . size ( ) ;
target = spritePtr - > pixels . data ( ) ;
return true ;
}
2012-06-29 16:44:26 +02:00
2023-10-09 19:12:16 -07:00
void GraphicManager : : addSpriteToCleanup ( GameSprite * spr ) {
2012-06-29 16:44:26 +02:00
cleanup_list . push_back ( spr ) ;
// Clean if needed
2023-10-09 19:12:16 -07:00
if ( cleanup_list . size ( ) > std : : max < uint32_t > ( 100 , g_settings . getInteger ( Config : : SOFTWARE_CLEAN_THRESHOLD ) ) ) {
for ( int i = 0 ; i < g_settings . getInteger ( Config : : SOFTWARE_CLEAN_SIZE ) & & static_cast < uint32_t > ( i ) < cleanup_list . size ( ) ; + + i ) {
2012-06-29 16:44:26 +02:00
cleanup_list . front ( ) - > unloadDC ( ) ;
cleanup_list . pop_front ( ) ;
}
}
}
2023-10-09 19:12:16 -07:00
void GraphicManager : : garbageCollection ( ) {
2026-04-22 19:19:35 -03:00
if ( ! g_settings . getInteger ( Config : : TEXTURE_MANAGEMENT ) ) {
return ;
}
auto now = std : : chrono : : steady_clock : : now ( ) ;
if ( now - lastclean < = std : : chrono : : seconds ( g_settings . getInteger ( Config : : TEXTURE_CLEAN_PULSE ) ) ) {
return ;
}
// Sheet GC runs independently of per-sprite texture threshold
auto longevity = std : : chrono : : seconds ( g_settings . getInteger ( Config : : TEXTURE_LONGEVITY ) ) ;
for ( const auto & sheet : g_spriteAppearances . getSheets ( ) ) {
if ( sheet - > glTextureId ! = 0 & & now - sheet - > lastaccess > longevity ) {
sheet - > releaseGLTexture ( ) ;
2012-06-29 16:44:26 +02:00
}
}
2026-04-22 19:19:35 -03:00
// Per-sprite image GC only runs when threshold is exceeded
if ( loaded_textures > g_settings . getInteger ( Config : : TEXTURE_CLEAN_THRESHOLD ) ) {
for ( auto iit = image_space . begin ( ) ; iit ! = image_space . end ( ) ; + + iit ) {
iit - > second - > clean ( now ) ;
}
}
lastclean = now ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
EditorSprite : : EditorSprite ( wxBitmap * b16x16 , wxBitmap * b32x32 ) {
2012-06-29 16:44:26 +02:00
bm [ SPRITE_SIZE_16x16 ] = b16x16 ;
bm [ SPRITE_SIZE_32x32 ] = b32x32 ;
}
2023-10-09 19:12:16 -07:00
EditorSprite : : ~ EditorSprite ( ) {
2012-06-29 16:44:26 +02:00
unloadDC ( ) ;
}
2023-10-09 19:12:16 -07:00
void EditorSprite : : DrawTo ( wxDC * dc , SpriteSize sz , int start_x , int start_y , int width , int height ) {
2012-06-29 16:44:26 +02:00
wxBitmap * sp = bm [ sz ] ;
2023-10-09 19:12:16 -07:00
if ( sp ) {
2012-06-29 16:44:26 +02:00
dc - > DrawBitmap ( * sp , start_x , start_y , true ) ;
2023-10-09 19:12:16 -07:00
}
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
void EditorSprite : : unloadDC ( ) {
2012-06-29 16:44:26 +02:00
delete bm [ SPRITE_SIZE_16x16 ] ;
delete bm [ SPRITE_SIZE_32x32 ] ;
2014-01-22 20:16:17 +01:00
bm [ SPRITE_SIZE_16x16 ] = nullptr ;
bm [ SPRITE_SIZE_32x32 ] = nullptr ;
2012-06-29 16:44:26 +02:00
}
GameSprite : : GameSprite ( ) :
id ( 0 ) ,
height ( 0 ) ,
width ( 0 ) ,
2015-02-03 22:59:14 -03:00
layers ( 0 ) ,
pattern_x ( 0 ) ,
pattern_y ( 0 ) ,
pattern_z ( 0 ) ,
2025-05-10 16:38:45 -03:00
sprite_phase_size ( 0 ) ,
2012-06-29 16:44:26 +02:00
numsprites ( 0 ) ,
2016-11-09 12:17:17 -03:00
animator ( nullptr ) ,
2012-06-29 16:44:26 +02:00
draw_height ( 0 ) ,
2023-10-09 19:12:16 -07:00
minimap_color ( 0 ) {
2025-05-10 16:38:45 -03:00
m_wxMemoryDc [ SPRITE_SIZE_16x16 ] = nullptr ;
m_wxMemoryDc [ SPRITE_SIZE_32x32 ] = nullptr ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
GameSprite : : ~ GameSprite ( ) {
2012-06-29 16:44:26 +02:00
unloadDC ( ) ;
2025-05-10 16:38:45 -03:00
delete animator ;
animator = nullptr ;
}
uint16_t GameSprite : : getDrawHeight ( ) const {
return draw_height ;
}
wxPoint GameSprite : : getDrawOffset ( ) {
if ( ! isDrawOffsetLoaded & & ! spriteList . empty ( ) ) {
2026-04-17 18:58:28 -03:00
const auto & sheet = g_spriteAppearances . getSheetBySpriteId ( spriteList [ 0 ] - > id ) ;
2025-05-10 16:38:45 -03:00
if ( ! sheet ) {
return wxPoint ( 0 , 0 ) ;
}
draw_offset . x + = sheet - > getSpriteSize ( ) . width - 32 ;
draw_offset . y + = sheet - > getSpriteSize ( ) . height - 32 ;
isDrawOffsetLoaded = true ;
2012-06-29 16:44:26 +02:00
}
2016-11-09 12:17:17 -03:00
2025-05-10 16:38:45 -03:00
return draw_offset ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
uint8_t GameSprite : : getWidth ( ) {
if ( width < = 0 ) {
2026-04-17 18:58:28 -03:00
const auto & sheet = g_spriteAppearances . getSheetBySpriteId ( spriteList [ 0 ] - > id , false ) ;
2025-05-10 16:38:45 -03:00
if ( sheet ) {
width = sheet - > getSpriteSize ( ) . width ;
height = sheet - > getSpriteSize ( ) . height ;
}
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
return width ;
}
uint8_t GameSprite : : getHeight ( ) {
if ( height < = 0 ) {
2026-04-17 18:58:28 -03:00
const auto & sheet = g_spriteAppearances . getSheetBySpriteId ( spriteList [ 0 ] - > id , false ) ;
2025-05-10 16:38:45 -03:00
if ( sheet ) {
width = sheet - > getSpriteSize ( ) . width ;
height = sheet - > getSpriteSize ( ) . height ;
}
}
return height ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
void GameSprite : : unloadDC ( ) {
2025-05-10 16:38:45 -03:00
delete m_wxMemoryDc [ SPRITE_SIZE_16x16 ] ;
delete m_wxMemoryDc [ SPRITE_SIZE_32x32 ] ;
m_wxMemoryDc [ SPRITE_SIZE_16x16 ] = nullptr ;
m_wxMemoryDc [ SPRITE_SIZE_32x32 ] = nullptr ;
}
uint8_t GameSprite : : getMiniMapColor ( ) const {
return minimap_color ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
int GameSprite : : getIndex ( int width , int height , int layer , int pattern_x , int pattern_y , int pattern_z , int frame ) const {
2026-04-17 18:58:28 -03:00
const int phase_count = std : : max < int > ( 1 , this - > sprite_phase_size ) ;
return ( ( ( ( frame % phase_count ) * this - > pattern_z + pattern_z ) * this - > pattern_y + pattern_y ) * this - > pattern_x + pattern_x ) * this - > layers + layer ;
2016-11-09 11:49:58 -03:00
}
2025-05-10 16:38:45 -03:00
GLuint GameSprite : : getHardwareID ( int _layer , int _count , int _pattern_x , int _pattern_y , int _pattern_z , int _frame ) {
2026-04-22 19:19:35 -03:00
if ( numsprites = = 0 | | spriteList . empty ( ) ) {
return 0 ;
}
const auto spriteCount = std : : min < uint32_t > ( numsprites , static_cast < uint32_t > ( spriteList . size ( ) ) ) ;
2026-04-17 18:58:28 -03:00
uint32_t v = _count > = 0
? static_cast < uint32_t > ( _count )
: static_cast < uint32_t > ( getIndex ( 0 , 0 , _layer , _pattern_x , _pattern_y , _pattern_z , _frame ) ) ;
2026-04-22 19:19:35 -03:00
if ( v > = spriteCount ) {
if ( spriteCount = = 1 ) {
2012-06-29 16:44:26 +02:00
v = 0 ;
} else {
2026-04-22 19:19:35 -03:00
v % = spriteCount ;
2012-06-29 16:44:26 +02:00
}
}
return spriteList [ v ] - > getHardwareID ( ) ;
}
2026-04-22 19:19:35 -03:00
SpriteUV GameSprite : : getAtlasUVs ( int _layer , int _count , int _pattern_x , int _pattern_y , int _pattern_z , int _frame ) {
if ( numsprites = = 0 | | spriteList . empty ( ) ) {
return { 0 , 0 , 1 , 1 } ;
}
const auto spriteCount = std : : min < uint32_t > ( numsprites , static_cast < uint32_t > ( spriteList . size ( ) ) ) ;
uint32_t v = _count > = 0
? static_cast < uint32_t > ( _count )
: static_cast < uint32_t > ( getIndex ( 0 , 0 , _layer , _pattern_x , _pattern_y , _pattern_z , _frame ) ) ;
if ( v > = spriteCount ) {
if ( spriteCount = = 1 ) {
v = 0 ;
} else {
v % = spriteCount ;
}
}
if ( const auto * img = spriteList [ v ] ; img ) {
return img - > getAtlasUVs ( ) ;
}
return { 0 , 0 , 1 , 1 } ;
}
2026-03-29 02:02:08 -03:00
std : : shared_ptr < GameSprite : : OutfitImage > GameSprite : : getOutfitImage ( int spriteId , int spriteIndex , const Outfit & outfit ) {
if ( layers > 1 & & ( uint32_t ) spriteIndex > = numsprites ) {
2025-05-10 16:38:45 -03:00
if ( numsprites = = 1 ) {
spriteIndex = 0 ;
} else {
spriteIndex % = numsprites ;
}
}
for ( auto & img : instanced_templates ) {
if ( img - > m_spriteId = = spriteId & & img - > m_spriteIndex = = spriteIndex ) {
2026-03-29 02:02:08 -03:00
if ( img - > m_outfit . getColorHash ( ) = = outfit . getColorHash ( ) ) {
2012-06-29 16:44:26 +02:00
return img ;
}
}
}
2025-05-10 16:38:45 -03:00
auto img = std : : make_shared < GameSprite : : OutfitImage > ( this , spriteIndex , spriteId , outfit ) ;
2012-06-29 16:44:26 +02:00
instanced_templates . push_back ( img ) ;
return img ;
}
2025-05-10 16:38:45 -03:00
wxMemoryDC * GameSprite : : getDC ( SpriteSize spriteSize ) {
ASSERT ( spriteSize = = SPRITE_SIZE_16x16 | | spriteSize = = SPRITE_SIZE_32x32 ) ;
if ( ! width & & ! height ) {
// Initialize default draw offset
2026-04-17 18:58:28 -03:00
const auto & sheet = g_spriteAppearances . getSheetBySpriteId ( spriteList [ 0 ] - > id , false ) ;
2025-05-10 16:38:45 -03:00
if ( sheet ) {
width = sheet - > getSpriteSize ( ) . width ;
height = sheet - > getSpriteSize ( ) . height ;
2012-06-29 16:44:26 +02:00
}
}
2016-11-09 11:49:58 -03:00
2025-05-10 16:38:45 -03:00
if ( ! m_wxMemoryDc [ spriteSize ] ) {
2016-11-09 11:49:58 -03:00
ASSERT ( width > = 1 & & height > = 1 ) ;
2025-05-10 16:38:45 -03:00
wxImage background ( getWidth ( ) , getHeight ( ) ) ;
auto backgroundBmp = wxBitmap ( background ) ;
m_wxMemoryDc [ spriteSize ] = new wxMemoryDC ( backgroundBmp ) ;
m_wxMemoryDc [ spriteSize ] - > SelectObject ( wxNullBitmap ) ;
2026-04-17 18:58:28 -03:00
auto spriteId = spriteList [ 0 ] - > id ;
2025-05-10 16:38:45 -03:00
wxImage wxImage = g_spriteAppearances . getWxImageBySpriteId ( spriteId ) ;
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
// Resize the image to rme::SpritePixels x rme::SpritePixels, if necessary
if ( getWidth ( ) > rme : : SpritePixels | | getHeight ( ) > rme : : SpritePixels ) {
wxImage . Rescale ( rme : : SpritePixels , rme : : SpritePixels ) ;
2012-06-29 16:44:26 +02:00
}
2016-11-09 11:49:58 -03:00
2025-05-10 16:38:45 -03:00
// Create a bitmap with the sprite image
auto bitMap = wxBitmap ( wxImage ) ;
m_wxMemoryDc [ spriteSize ] - > SelectObject ( bitMap ) ;
2016-11-09 11:49:58 -03:00
g_gui . gfx . addSpriteToCleanup ( this ) ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
return m_wxMemoryDc [ spriteSize ] ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
void GameSprite : : DrawTo ( wxDC * dcWindow , SpriteSize spriteSize , int start_x , int start_y , int sizeWidth , int sizeHeight ) {
if ( sizeWidth = = - 1 | | sizeHeight = = - 1 ) {
if ( spriteList . size ( ) = = 0 ) {
return ;
}
2026-04-17 18:58:28 -03:00
const auto & sheet = g_spriteAppearances . getSheetBySpriteId ( spriteList [ 0 ] - > id ) ;
2025-05-10 16:38:45 -03:00
if ( ! sheet ) {
return ;
}
sizeWidth = sheet - > getSpriteSize ( ) . width ;
sizeHeight = sheet - > getSpriteSize ( ) . height ;
2023-10-09 19:12:16 -07:00
}
2025-05-10 16:38:45 -03:00
wxMemoryDC * sdc = getDC ( spriteSize ) ;
2023-10-09 19:12:16 -07:00
if ( sdc ) {
2025-05-10 16:38:45 -03:00
dcWindow - > Blit ( start_x , start_y , sizeWidth , sizeHeight , sdc , 0 , 0 , wxCOPY , true ) ;
2015-12-06 11:42:37 -03:00
} else {
2025-05-10 16:38:45 -03:00
const wxBrush & b = dcWindow - > GetBrush ( ) ;
dcWindow - > SetBrush ( * wxRED_BRUSH ) ;
dcWindow - > DrawRectangle ( start_x , start_y , sizeWidth , sizeHeight ) ;
dcWindow - > SetBrush ( b ) ;
2012-06-29 16:44:26 +02:00
}
}
2023-10-09 19:12:16 -07:00
GameSprite : : Image : : ~ Image ( ) {
2012-06-29 16:44:26 +02:00
unloadGLTexture ( 0 ) ;
}
2023-10-09 19:12:16 -07:00
void GameSprite : : Image : : createGLTexture ( GLuint textureId ) {
2026-04-22 19:19:35 -03:00
// No-op: base class texture creation no longer used (subclasses override)
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
void GameSprite : : Image : : unloadGLTexture ( GLuint textureId ) {
2012-06-29 16:44:26 +02:00
isGLLoaded = false ;
2016-09-29 20:30:54 -03:00
g_gui . gfx . loaded_textures - = 1 ;
2026-04-17 18:58:28 -03:00
GLRenderer : : invalidateTexture ( textureId ) ;
2023-10-09 23:02:37 -03:00
glDeleteTextures ( 1 , & textureId ) ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
void GameSprite : : Image : : visit ( ) {
2026-04-22 19:19:35 -03:00
lastaccess = std : : chrono : : steady_clock : : now ( ) ;
2012-06-29 16:44:26 +02:00
}
2026-04-22 19:19:35 -03:00
void GameSprite : : Image : : clean ( std : : chrono : : steady_clock : : time_point now ) {
if ( isGLLoaded & & now - lastaccess > std : : chrono : : seconds ( g_settings . getInteger ( Config : : TEXTURE_LONGEVITY ) ) ) {
2012-06-29 16:44:26 +02:00
unloadGLTexture ( 0 ) ;
}
}
2026-04-22 19:19:35 -03:00
void GameSprite : : NormalImage : : clean ( std : : chrono : : steady_clock : : time_point now ) {
// Check if the GL texture sheet is still valid
if ( atlasTextureId ! = 0 ) {
auto sheet = g_spriteAppearances . getSheetBySpriteId ( id , false ) ;
if ( ! sheet | | sheet - > glTextureId = = 0 ) {
atlasTextureId = 0 ;
}
}
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
GameSprite : : NormalImage : : ~ NormalImage ( ) {
2025-05-10 16:38:45 -03:00
m_cachedData = nullptr ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
uint8_t * GameSprite : : NormalImage : : getRGBAData ( ) {
2025-05-10 16:38:45 -03:00
if ( ! m_cachedData ) {
if ( ! g_gui . gfx . loadSpriteDump ( m_cachedData , size , id ) ) {
spdlog : : error ( " [GameSprite::NormalImage::getRGBAData] - Failed when parsing sprite id {} " , id ) ;
2016-11-09 11:49:58 -03:00
return nullptr ;
2012-06-29 16:44:26 +02:00
}
}
2025-05-10 16:38:45 -03:00
return m_cachedData ;
2012-06-29 16:44:26 +02:00
}
2023-10-09 19:12:16 -07:00
GLuint GameSprite : : NormalImage : : getHardwareID ( ) {
2026-04-22 19:19:35 -03:00
auto sheet = g_spriteAppearances . getSheetBySpriteId ( id ) ;
if ( ! sheet ) {
visit ( ) ;
return 0 ;
}
GLuint currentAtlasTextureId = sheet - > getOrUploadGLTexture ( ) ;
if ( currentAtlasTextureId = = 0 ) {
atlasTextureId = 0 ;
visit ( ) ;
return 0 ;
}
if ( atlasTextureId ! = currentAtlasTextureId ) {
auto uvs = sheet - > getSpriteUVs ( id ) ;
atlasU0 = uvs . u0 ;
atlasV0 = uvs . v0 ;
atlasU1 = uvs . u1 ;
atlasV1 = uvs . v1 ;
atlasTextureId = currentAtlasTextureId ;
2012-06-29 16:44:26 +02:00
}
2026-04-22 19:19:35 -03:00
sheet - > lastaccess = std : : chrono : : steady_clock : : now ( ) ;
2012-06-29 16:44:26 +02:00
visit ( ) ;
2026-04-22 19:19:35 -03:00
return atlasTextureId ;
2026-04-17 18:58:28 -03:00
}
feat: add Cyclopedia protobuf export pipeline (#160)
Add editor support for exporting Cyclopedia and CipSoft protobuf assets from the currently loaded map.
Main changes:
- Add File > Client Assets actions for Static House Data export, Cyclopedia minimap and satellite export, and restoring timestamped client asset backups.
- Keep generic minimap and tileset exports under File > Export.
- Export staticdata, staticmapdata, and mapdata protobuf files with content-hashed filenames.
- Update catalog-content.json so generated assets are picked up by the client.
- Merge compatible CipSoft templates when they match the loaded map.
- Fall back to generated data for custom maps instead of failing the export.
- Support filtered house exports.
- Report warnings for missing or failed static map entries.
- Emit aligned MINIMAP and sprite-based SATELLITE assets for the documented scales, including the 1/16 layer.
- Keep Cyclopedia export progress in the status bar instead of using a blocking modal progress dialog.
Performance:
- Precompute floor and chunk plans for global map exports.
- Write BMP output directly.
- Reuse minimap renders for matching satellite assets.
- Parallelize asset hash and LZMA encoding through a bounded worker queue.
- Remove modal progress overhead from the export path.
- Revert the tile-grid satellite optimization after profiling showed it shifted cost without improving total export time.
Documentation:
- Document the export contract in docs/static-data.md.
- Add docs/cyclopedia-export-roadmap.md with deferred optimization candidates and follow-up notes.
Scope:
- Focus this change on the Cyclopedia/staticdata protobuf export path and the export-specific performance work needed for practical global map exports.
- Move unrelated runtime, performance, and UI experiments out of this branch.
Validation:
- Ran static inspection and git diff checks during the export changes.
- Reviewed Visual Studio CPU profiles after each global map export performance pass.
- Verified hash and LZMA work moved into bounded async workers.
- Verified the restore action is labeled as restore and grouped under File > Client Assets, not File > Export.
This adds the editor-side pipeline needed to generate client Cyclopedia assets from a loaded map while keeping the export flow recoverable, documented, and practical for large maps.
2026-05-28 08:35:21 -03:00
uint32_t GameSprite : : getSpriteID ( int _layer , int _count , int _pattern_x , int _pattern_y , int _pattern_z , int _frame ) {
2026-04-17 18:58:28 -03:00
uint32_t v ;
if ( _count > = 0 ) {
v = _count ;
} else {
feat: add Cyclopedia protobuf export pipeline (#160)
Add editor support for exporting Cyclopedia and CipSoft protobuf assets from the currently loaded map.
Main changes:
- Add File > Client Assets actions for Static House Data export, Cyclopedia minimap and satellite export, and restoring timestamped client asset backups.
- Keep generic minimap and tileset exports under File > Export.
- Export staticdata, staticmapdata, and mapdata protobuf files with content-hashed filenames.
- Update catalog-content.json so generated assets are picked up by the client.
- Merge compatible CipSoft templates when they match the loaded map.
- Fall back to generated data for custom maps instead of failing the export.
- Support filtered house exports.
- Report warnings for missing or failed static map entries.
- Emit aligned MINIMAP and sprite-based SATELLITE assets for the documented scales, including the 1/16 layer.
- Keep Cyclopedia export progress in the status bar instead of using a blocking modal progress dialog.
Performance:
- Precompute floor and chunk plans for global map exports.
- Write BMP output directly.
- Reuse minimap renders for matching satellite assets.
- Parallelize asset hash and LZMA encoding through a bounded worker queue.
- Remove modal progress overhead from the export path.
- Revert the tile-grid satellite optimization after profiling showed it shifted cost without improving total export time.
Documentation:
- Document the export contract in docs/static-data.md.
- Add docs/cyclopedia-export-roadmap.md with deferred optimization candidates and follow-up notes.
Scope:
- Focus this change on the Cyclopedia/staticdata protobuf export path and the export-specific performance work needed for practical global map exports.
- Move unrelated runtime, performance, and UI experiments out of this branch.
Validation:
- Ran static inspection and git diff checks during the export changes.
- Reviewed Visual Studio CPU profiles after each global map export performance pass.
- Verified hash and LZMA work moved into bounded async workers.
- Verified the restore action is labeled as restore and grouped under File > Client Assets, not File > Export.
This adds the editor-side pipeline needed to generate client Cyclopedia assets from a loaded map while keeping the export flow recoverable, documented, and practical for large maps.
2026-05-28 08:35:21 -03:00
v = static_cast < uint32_t > ( getIndex ( 0 , 0 , _layer , _pattern_x , _pattern_y , _pattern_z , _frame ) ) ;
2026-04-17 18:58:28 -03:00
}
if ( v > = numsprites ) {
if ( numsprites = = 1 ) {
v = 0 ;
} else {
v % = numsprites ;
}
}
return spriteList [ v ] - > id ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
void GameSprite : : NormalImage : : createGLTexture ( GLuint ) {
2026-04-22 19:19:35 -03:00
// no-op: NormalImage agora usa atlas via getHardwareID()
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
void GameSprite : : NormalImage : : unloadGLTexture ( GLuint ) {
2026-04-22 19:19:35 -03:00
// No-op: atlas textures are managed by SpriteSheet::releaseGLTexture
2023-10-09 23:02:37 -03:00
}
2023-10-09 19:12:16 -07:00
GameSprite : : EditorImage : : EditorImage ( const wxArtID & bitmapId ) :
2023-10-09 23:02:37 -03:00
NormalImage ( ) ,
2023-10-09 19:12:16 -07:00
bitmapId ( bitmapId ) { }
2023-10-09 23:02:37 -03:00
2026-04-22 19:19:35 -03:00
GLuint GameSprite : : EditorImage : : getHardwareID ( ) {
if ( ! isGLLoaded ) {
createGLTexture ( 0 ) ;
}
visit ( ) ;
return glTextureId ;
}
2023-10-09 19:12:16 -07:00
void GameSprite : : EditorImage : : createGLTexture ( GLuint textureId ) {
2023-10-09 23:02:37 -03:00
ASSERT ( ! isGLLoaded ) ;
wxSize size ( rme : : SpritePixels , rme : : SpritePixels ) ;
wxBitmap bitmap = wxArtProvider : : GetBitmap ( bitmapId , wxART_OTHER , size ) ;
wxNativePixelData data ( bitmap ) ;
2023-10-09 19:12:16 -07:00
if ( ! data ) {
return ;
}
2023-10-09 23:02:37 -03:00
const int imageSize = rme : : SpritePixelsSize * 4 ;
2023-10-09 19:12:16 -07:00
GLubyte * imageData = new GLubyte [ imageSize ] ;
2023-10-09 23:02:37 -03:00
int write = 0 ;
wxNativePixelData : : Iterator it ( data ) ;
it . Offset ( data , 0 , 0 ) ;
2023-10-09 19:12:16 -07:00
for ( size_t y = 0 ; y < rme : : SpritePixels ; + + y ) {
2023-10-09 23:02:37 -03:00
wxNativePixelData : : Iterator row_start = it ;
2023-10-09 19:12:16 -07:00
for ( size_t x = 0 ; x < rme : : SpritePixels ; + + x , it + + ) {
2023-10-09 23:02:37 -03:00
uint8_t red = it . Red ( ) ;
uint8_t green = it . Green ( ) ;
uint8_t blue = it . Blue ( ) ;
bool transparent = red = = 0xFF & & green = = 0x00 & & blue = = 0xFF ;
imageData [ write + 0 ] = red ;
imageData [ write + 1 ] = green ;
imageData [ write + 2 ] = blue ;
imageData [ write + 3 ] = transparent ? 0x00 : 0xFF ;
write + = 4 ;
}
it = row_start ;
it . OffsetY ( data , 1 ) ;
}
isGLLoaded = true ;
2026-04-17 18:58:28 -03:00
glTextureId = g_gui . gfx . getFreeTextureID ( ) ;
2023-10-09 23:02:37 -03:00
g_gui . gfx . loaded_textures + = 1 ;
2026-04-17 18:58:28 -03:00
glBindTexture ( GL_TEXTURE_2D , glTextureId ) ;
2025-05-10 16:38:45 -03:00
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER , GL_NEAREST ) ; // Nearest Filtering
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_NEAREST ) ; // Nearest Filtering
2026-04-17 18:58:28 -03:00
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_WRAP_S , GL_CLAMP_TO_EDGE ) ;
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_WRAP_T , GL_CLAMP_TO_EDGE ) ;
2023-10-09 23:02:37 -03:00
glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGBA , rme : : SpritePixels , rme : : SpritePixels , 0 , GL_RGBA , GL_UNSIGNED_BYTE , imageData ) ;
delete [ ] imageData ;
}
2023-10-09 19:12:16 -07:00
void GameSprite : : EditorImage : : unloadGLTexture ( GLuint textureId ) {
2026-04-17 18:58:28 -03:00
Image : : unloadGLTexture ( glTextureId ) ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
// OutfitImage
GameSprite : : OutfitImage : : OutfitImage ( GameSprite * initParent , int initSpriteIndex , GLuint initSpriteId , const Outfit & initOutfit ) :
m_spriteId ( initSpriteId ) ,
m_spriteIndex ( initSpriteIndex ) ,
m_parent ( initParent ) ,
m_outfit ( initOutfit ) { }
GameSprite : : OutfitImage : : ~ OutfitImage ( ) {
2026-03-29 02:02:08 -03:00
delete [ ] m_cachedOutfitData ;
2025-05-10 16:38:45 -03:00
m_cachedOutfitData = nullptr ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
void GameSprite : : OutfitImage : : unloadGLTexture ( GLuint ) {
2026-04-17 18:58:28 -03:00
if ( m_textureId ! = 0 ) {
isGLLoaded = false ;
m_isGLLoaded = false ;
g_gui . gfx . loaded_textures - = 1 ;
GLRenderer : : invalidateTexture ( m_textureId ) ;
glDeleteTextures ( 1 , & m_textureId ) ;
m_textureId = 0 ;
}
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
void GameSprite : : OutfitImage : : colorizePixel ( uint8_t color , uint8_t & red , uint8_t & green , uint8_t & blue ) {
2012-06-29 16:44:26 +02:00
uint8_t ro = ( TemplateOutfitLookupTable [ color ] & 0xFF0000 ) > > 16 ; // rgb outfit
uint8_t go = ( TemplateOutfitLookupTable [ color ] & 0xFF00 ) > > 8 ;
uint8_t bo = ( TemplateOutfitLookupTable [ color ] & 0xFF ) ;
2025-05-10 16:38:45 -03:00
red = ( uint8_t ) ( red * ( ( float ) ro / 255.f ) ) ;
green = ( uint8_t ) ( green * ( ( float ) go / 255.f ) ) ;
blue = ( uint8_t ) ( blue * ( ( float ) bo / 255.f ) ) ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
uint8_t * GameSprite : : OutfitImage : : getRGBAData ( ) {
if ( m_cachedOutfitData ) {
return m_cachedOutfitData ;
}
2012-06-29 16:44:26 +02:00
2026-04-17 18:58:28 -03:00
const auto & sprite = g_spriteAppearances . getSprite ( m_parent - > spriteList [ m_spriteIndex ] - > id ) ;
2025-05-10 16:38:45 -03:00
if ( ! sprite ) {
2014-01-22 20:16:17 +01:00
return nullptr ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
const auto offBounds = m_parent - > spriteList . size ( ) < = m_spriteIndex + 1 ;
const auto templateIndex = offBounds ? m_spriteIndex : m_spriteIndex + 1 ;
2026-04-17 18:58:28 -03:00
const auto & spriteTemplate = g_spriteAppearances . getSprite ( m_parent - > spriteList [ templateIndex ] - > id ) ;
2025-05-10 16:38:45 -03:00
if ( ! spriteTemplate ) {
2014-01-22 20:16:17 +01:00
return nullptr ;
2012-06-29 16:44:26 +02:00
}
2026-03-29 02:02:08 -03:00
int height = sprite - > size . height ;
int width = sprite - > size . width ;
auto totalSize = width * height * 4 ;
auto rgbadata = std : : make_unique < uint8_t [ ] > ( totalSize ) ;
std : : memcpy ( rgbadata . get ( ) , sprite - > pixels . data ( ) , totalSize ) ;
2025-05-10 16:38:45 -03:00
uint8_t * template_rgbadata = spriteTemplate - > pixels . data ( ) ;
if ( m_outfit . lookHead > ( sizeof ( TemplateOutfitLookupTable ) / sizeof ( TemplateOutfitLookupTable [ 0 ] ) ) ) {
m_outfit . lookHead = 0 ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
if ( m_outfit . lookBody > ( sizeof ( TemplateOutfitLookupTable ) / sizeof ( TemplateOutfitLookupTable [ 0 ] ) ) ) {
m_outfit . lookBody = 0 ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
if ( m_outfit . lookLegs > ( sizeof ( TemplateOutfitLookupTable ) / sizeof ( TemplateOutfitLookupTable [ 0 ] ) ) ) {
m_outfit . lookLegs = 0 ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
if ( m_outfit . lookFeet > ( sizeof ( TemplateOutfitLookupTable ) / sizeof ( TemplateOutfitLookupTable [ 0 ] ) ) ) {
m_outfit . lookFeet = 0 ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
for ( int y = 0 ; y < height ; + + y ) {
for ( int x = 0 ; x < width ; + + x ) {
const int index = ( y * width + x ) * 4 ;
uint8_t & red = rgbadata [ index + 2 ] ;
uint8_t & green = rgbadata [ index + 1 ] ;
uint8_t & blue = rgbadata [ index ] ;
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
const uint8_t & tred = template_rgbadata [ index + 2 ] ;
const uint8_t & tgreen = template_rgbadata [ index + 1 ] ;
const uint8_t & tblue = template_rgbadata [ index ] ;
2012-06-29 16:44:26 +02:00
2023-10-09 19:12:16 -07:00
if ( tred & & tgreen & & ! tblue ) { // yellow => head
2025-05-10 16:38:45 -03:00
colorizePixel ( m_outfit . lookHead , red , green , blue ) ;
2023-10-09 19:12:16 -07:00
} else if ( tred & & ! tgreen & & ! tblue ) { // red => body
2025-05-10 16:38:45 -03:00
colorizePixel ( m_outfit . lookBody , red , green , blue ) ;
2023-10-09 19:12:16 -07:00
} else if ( ! tred & & tgreen & & ! tblue ) { // green => legs
2025-05-10 16:38:45 -03:00
colorizePixel ( m_outfit . lookLegs , red , green , blue ) ;
2023-10-09 19:12:16 -07:00
} else if ( ! tred & & ! tgreen & & tblue ) { // blue => feet
2025-05-10 16:38:45 -03:00
colorizePixel ( m_outfit . lookFeet , red , green , blue ) ;
2012-06-29 16:44:26 +02:00
}
}
}
2025-05-10 16:38:45 -03:00
spdlog : : debug ( " outfit name: {}, pattern_x: {}, pattern_y: {}, pattern_z: {}, sprite_phase_size: {}, layers: {}, draw height: {}, drawx: {}, drawy: {} " , m_outfit . name , m_parent - > pattern_x , m_parent - > pattern_y , m_parent - > pattern_z , m_parent - > sprite_phase_size , m_parent - > layers , m_parent - > draw_height , m_parent - > getDrawOffset ( ) . x , m_parent - > getDrawOffset ( ) . y ) ;
2012-06-29 16:44:26 +02:00
2026-03-29 02:02:08 -03:00
m_cachedOutfitData = rgbadata . release ( ) ;
2025-05-10 16:38:45 -03:00
return m_cachedOutfitData ;
}
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
GLuint GameSprite : : OutfitImage : : getHardwareID ( ) {
if ( ! m_isGLLoaded ) {
if ( m_textureId = = 0 ) {
m_textureId = g_gui . gfx . getFreeTextureID ( ) ;
}
createGLTexture ( m_spriteId , m_textureId ) ;
if ( ! m_isGLLoaded ) {
return 0 ;
}
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
return m_textureId ;
}
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
void GameSprite : : OutfitImage : : createGLTexture ( GLuint spriteId , GLuint textureId ) {
ASSERT ( ! m_isGLLoaded ) ;
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
uint8_t * rgba = getRGBAData ( ) ;
if ( ! rgba ) {
return ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
const auto & sheet = g_spriteAppearances . getSheetBySpriteId ( spriteId ) ;
if ( ! sheet ) {
return ;
2012-06-29 16:44:26 +02:00
}
2025-05-10 16:38:45 -03:00
auto spriteWidth = sheet - > getSpriteSize ( ) . width ;
auto spriteHeight = sheet - > getSpriteSize ( ) . height ;
m_isGLLoaded = true ;
g_gui . gfx . loaded_textures + = 1 ;
2012-06-29 16:44:26 +02:00
2025-05-10 16:38:45 -03:00
glBindTexture ( GL_TEXTURE_2D , textureId > 0 ? textureId : spriteId ) ;
2026-04-22 19:19:35 -03:00
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER , GL_NEAREST ) ;
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_NEAREST ) ;
2026-04-17 18:58:28 -03:00
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_WRAP_S , GL_CLAMP_TO_EDGE ) ;
glTexParameteri ( GL_TEXTURE_2D , GL_TEXTURE_WRAP_T , GL_CLAMP_TO_EDGE ) ;
2026-04-22 19:19:35 -03:00
glTexImage2D ( GL_TEXTURE_2D , 0 , GL_RGBA , spriteWidth , spriteHeight , 0 , GL_BGRA , GL_UNSIGNED_BYTE , rgba ) ;
2012-06-29 16:44:26 +02:00
}
2016-11-09 12:17:17 -03:00
2023-10-09 19:12:16 -07:00
GameSprite * GameSprite : : createFromBitmap ( const wxArtID & bitmapId ) {
2023-10-09 23:02:37 -03:00
GameSprite : : EditorImage * image = new GameSprite : : EditorImage ( bitmapId ) ;
GameSprite * sprite = new GameSprite ( ) ;
sprite - > width = 1 ;
sprite - > height = 1 ;
sprite - > layers = 1 ;
sprite - > pattern_x = 1 ;
sprite - > pattern_y = 1 ;
sprite - > pattern_z = 1 ;
2025-05-10 16:38:45 -03:00
sprite - > sprite_phase_size = 1 ;
2023-10-09 23:02:37 -03:00
sprite - > numsprites = 1 ;
sprite - > spriteList . push_back ( image ) ;
return sprite ;
}
2016-11-09 12:17:17 -03:00
// ============================================================================
// Animator
2026-04-14 13:39:40 -03:00
// ============================================================================
2016-11-09 12:17:17 -03:00
Animator : : Animator ( int frame_count , int start_frame , int loop_count , bool async ) :
frame_count ( frame_count ) ,
start_frame ( start_frame ) ,
loop_count ( loop_count ) ,
async ( async ) ,
current_frame ( 0 ) ,
current_loop ( 0 ) ,
current_duration ( 0 ) ,
total_duration ( 0 ) ,
direction ( ANIMATION_FORWARD ) ,
last_time ( 0 ) ,
2023-10-09 19:12:16 -07:00
is_complete ( false ) {
2016-11-09 12:17:17 -03:00
ASSERT ( start_frame > = - 1 & & start_frame < frame_count ) ;
2023-10-09 19:12:16 -07:00
for ( int i = 0 ; i < frame_count ; i + + ) {
2016-11-09 12:17:17 -03:00
durations . push_back ( newd FrameDuration ( ITEM_FRAME_DURATION , ITEM_FRAME_DURATION ) ) ;
}
reset ( ) ;
}
2023-10-09 19:12:16 -07:00
Animator : : ~ Animator ( ) {
for ( int i = 0 ; i < frame_count ; i + + ) {
2016-11-09 12:17:17 -03:00
delete durations [ i ] ;
2025-05-10 16:38:45 -03:00
durations [ i ] = nullptr ;
2016-11-09 12:17:17 -03:00
}
durations . clear ( ) ;
}
2023-10-09 19:12:16 -07:00
int Animator : : getStartFrame ( ) const {
if ( start_frame > - 1 ) {
2016-11-09 12:17:17 -03:00
return start_frame ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
return uniform_random ( 0 , frame_count - 1 ) ;
}
2023-10-09 19:12:16 -07:00
FrameDuration * Animator : : getFrameDuration ( int frame ) {
2016-11-09 12:17:17 -03:00
ASSERT ( frame > = 0 & & frame < frame_count ) ;
return durations [ frame ] ;
}
2023-10-09 19:12:16 -07:00
int Animator : : getFrame ( ) {
2016-11-09 12:17:17 -03:00
long time = g_gui . gfx . getElapsedTime ( ) ;
2023-10-09 19:12:16 -07:00
if ( time ! = last_time & & ! is_complete ) {
2016-11-09 12:17:17 -03:00
long elapsed = time - last_time ;
2023-10-09 19:12:16 -07:00
if ( elapsed > = current_duration ) {
2016-11-09 12:17:17 -03:00
int frame = 0 ;
2023-10-09 19:12:16 -07:00
if ( loop_count < 0 ) {
2016-11-09 12:17:17 -03:00
frame = getPingPongFrame ( ) ;
2023-10-09 19:12:16 -07:00
} else {
2016-11-09 12:17:17 -03:00
frame = getLoopFrame ( ) ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
2023-10-09 19:12:16 -07:00
if ( current_frame ! = frame ) {
2016-11-09 12:17:17 -03:00
int duration = getDuration ( frame ) - ( elapsed - current_duration ) ;
2023-10-09 19:12:16 -07:00
if ( duration < 0 & & ! async ) {
2016-11-09 12:17:17 -03:00
calculateSynchronous ( ) ;
} else {
current_frame = frame ;
current_duration = std : : max < int > ( 0 , duration ) ;
}
} else {
is_complete = true ;
}
} else {
current_duration - = elapsed ;
}
last_time = time ;
}
return current_frame ;
}
2023-10-09 19:12:16 -07:00
void Animator : : setFrame ( int frame ) {
2016-11-09 12:17:17 -03:00
ASSERT ( frame = = - 1 | | frame = = 255 | | frame = = 254 | | ( frame > = 0 & & frame < frame_count ) ) ;
2023-10-09 19:12:16 -07:00
if ( current_frame = = frame ) {
2016-11-09 12:17:17 -03:00
return ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
2023-10-09 19:12:16 -07:00
if ( async ) {
if ( frame = = 255 ) { // Async mode
2016-11-09 12:17:17 -03:00
current_frame = 0 ;
2023-10-09 19:12:16 -07:00
} else if ( frame = = 254 ) { // Random mode
2016-11-09 12:17:17 -03:00
current_frame = uniform_random ( 0 , frame_count - 1 ) ;
2023-10-09 19:12:16 -07:00
} else if ( frame > = 0 & & frame < frame_count ) {
2016-11-09 12:17:17 -03:00
current_frame = frame ;
2023-10-09 19:12:16 -07:00
} else {
2016-11-09 12:17:17 -03:00
current_frame = getStartFrame ( ) ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
is_complete = false ;
last_time = g_gui . gfx . getElapsedTime ( ) ;
current_duration = getDuration ( current_frame ) ;
current_loop = 0 ;
} else {
calculateSynchronous ( ) ;
}
}
2023-10-09 19:12:16 -07:00
void Animator : : reset ( ) {
2016-11-09 12:17:17 -03:00
total_duration = 0 ;
2023-10-09 19:12:16 -07:00
for ( int i = 0 ; i < frame_count ; i + + ) {
2016-11-09 12:17:17 -03:00
total_duration + = durations [ i ] - > max ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
is_complete = false ;
direction = ANIMATION_FORWARD ;
current_loop = 0 ;
2017-05-06 12:31:26 -03:00
async = false ;
2016-11-09 12:17:17 -03:00
setFrame ( - 1 ) ;
}
2023-10-09 19:12:16 -07:00
int Animator : : getDuration ( int frame ) const {
2016-11-09 12:17:17 -03:00
ASSERT ( frame > = 0 & & frame < frame_count ) ;
return durations [ frame ] - > getDuration ( ) ;
}
2023-10-09 19:12:16 -07:00
int Animator : : getPingPongFrame ( ) {
2016-11-09 12:17:17 -03:00
int count = direction = = ANIMATION_FORWARD ? 1 : - 1 ;
int next_frame = current_frame + count ;
2023-10-09 19:12:16 -07:00
if ( next_frame < 0 | | next_frame > = frame_count ) {
2016-11-09 12:17:17 -03:00
direction = direction = = ANIMATION_FORWARD ? ANIMATION_BACKWARD : ANIMATION_FORWARD ;
count * = - 1 ;
}
return current_frame + count ;
}
2023-10-09 19:12:16 -07:00
int Animator : : getLoopFrame ( ) {
2016-11-09 12:17:17 -03:00
int next_phase = current_frame + 1 ;
2023-10-09 19:12:16 -07:00
if ( next_phase < frame_count ) {
2016-11-09 12:17:17 -03:00
return next_phase ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
2023-10-09 19:12:16 -07:00
if ( loop_count = = 0 ) {
2016-11-09 12:17:17 -03:00
return 0 ;
2023-10-09 19:12:16 -07:00
}
2016-11-09 12:17:17 -03:00
2023-10-09 19:12:16 -07:00
if ( current_loop < ( loop_count - 1 ) ) {
2016-11-09 12:17:17 -03:00
current_loop + + ;
return 0 ;
}
return current_frame ;
}
2023-10-09 19:12:16 -07:00
void Animator : : calculateSynchronous ( ) {
2016-11-09 12:17:17 -03:00
long time = g_gui . gfx . getElapsedTime ( ) ;
2023-10-09 19:12:16 -07:00
if ( time > 0 & & total_duration > 0 ) {
2016-11-09 12:17:17 -03:00
long elapsed = time % total_duration ;
int total_time = 0 ;
2023-10-09 19:12:16 -07:00
for ( int i = 0 ; i < frame_count ; i + + ) {
2016-11-09 12:17:17 -03:00
int duration = getDuration ( i ) ;
2023-10-09 19:12:16 -07:00
if ( elapsed > = total_time & & elapsed < total_time + duration ) {
2016-11-09 12:17:17 -03:00
current_frame = i ;
current_duration = duration - ( elapsed - total_time ) ;
break ;
}
total_time + = duration ;
}
last_time = time ;
}
}