diff --git a/resources/unix/easyrpg-player.6.adoc b/resources/unix/easyrpg-player.6.adoc index cffb48af30..0a4081d08e 100644 --- a/resources/unix/easyrpg-player.6.adoc +++ b/resources/unix/easyrpg-player.6.adoc @@ -89,11 +89,20 @@ NOTE: For games that only use ASCII (English games) use '1252'. Enable limited support for the DynRPG patch from Cherry. The patches are not loaded from DLL files, but re-implemented by the engine. +*--patch-easyrpg*:: + Enable EasyRPG extensions such as support for 32 bit images and large charsets. + *--patch-key-patch*:: Enable support for the Key Patch by Ineluki. -*--patch-maniac*:: +*--patch-maniac*:: _[N]_ Enable support for the Maniac Patch by BingShan. + Values for N: + - 1: Enable the patch (default) + - 2: Enable the patch but do not adjust variable ranges to 32 bit. + + Not adjusting the variable ranges is useful if you are adding the patch to an + existing game, as this reduces the likelihood that the game will stop working. *--patch-pic-unlock*:: Picture movement is not interrupted by messages in any version of the engine. diff --git a/src/battle_animation.cpp b/src/battle_animation.cpp index 6c552bbd1a..ee0fe7b388 100644 --- a/src/battle_animation.cpp +++ b/src/battle_animation.cpp @@ -264,7 +264,7 @@ void BattleAnimationMap::DrawSingle(Bitmap& dst) { } const int character_height = 24; int x_off = target.GetScreenX(); - int y_off = target.GetScreenY(false, false); + int y_off = target.GetScreenY(false); if (Scene::instance->type == Scene::Map) { x_off += static_cast(Scene::instance.get())->spriteset->GetRenderOx(); y_off += static_cast(Scene::instance.get())->spriteset->GetRenderOy(); diff --git a/src/bitmap.cpp b/src/bitmap.cpp index 25992d6366..d74a358e95 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -99,9 +99,7 @@ Bitmap::Bitmap(Filesystem_Stream::InputStream stream, bool transparent, uint32_t return; } - int w = 0; - int h = 0; - void* pixels = nullptr; + ImageOut image_out; uint8_t data[4] = {}; size_t bytes = stream.read(reinterpret_cast(data), 4).gcount(); @@ -109,27 +107,28 @@ Bitmap::Bitmap(Filesystem_Stream::InputStream stream, bool transparent, uint32_t bool img_okay = false; - if (bytes >= 4 && strncmp((char*)data, "XYZ1", 4) == 0) - img_okay = ImageXYZ::ReadXYZ(stream, transparent, w, h, pixels); - else if (bytes > 2 && strncmp((char*)data, "BM", 2) == 0) - img_okay = ImageBMP::ReadBMP(stream, transparent, w, h, pixels); - else if (bytes >= 4 && strncmp((char*)(data + 1), "PNG", 3) == 0) - img_okay = ImagePNG::ReadPNG(stream, transparent, w, h, pixels); - else + if (bytes >= 4 && strncmp((char*)data, "XYZ1", 4) == 0) { + img_okay = ImageXYZ::Read(stream, transparent, image_out); + } else if (bytes > 2 && strncmp((char*)data, "BM", 2) == 0) { + img_okay = ImageBMP::Read(stream, transparent, image_out); + } else if (bytes >= 4 && strncmp((char*)(data + 1), "PNG", 3) == 0) { + img_okay = ImagePNG::Read(stream, transparent, image_out); + } else Output::Warning("Unsupported image file {} (Magic: {:02X})", stream.GetName(), *reinterpret_cast(data)); if (!img_okay) { - free(pixels); - pixels = nullptr; + free(image_out.pixels); return; } - Init(w, h, nullptr); + Init(image_out.width, image_out.height, nullptr); - ConvertImage(w, h, pixels, transparent); + ConvertImage(image_out.width, image_out.height, image_out.pixels, transparent); CheckPixels(flags); + original_bpp = image_out.bpp; + filename = ToString(stream.GetName()); } @@ -137,29 +136,29 @@ Bitmap::Bitmap(const uint8_t* data, unsigned bytes, bool transparent, uint32_t f format = (transparent ? pixel_format : opaque_pixel_format); pixman_format = find_format(format); - int w = 0, h = 0; - void* pixels = nullptr; + ImageOut image_out; bool img_okay = false; if (bytes > 4 && strncmp((char*) data, "XYZ1", 4) == 0) - img_okay = ImageXYZ::ReadXYZ(data, bytes, transparent, w, h, pixels); + img_okay = ImageXYZ::Read(data, bytes, transparent, image_out); else if (bytes > 2 && strncmp((char*) data, "BM", 2) == 0) - img_okay = ImageBMP::ReadBMP(data, bytes, transparent, w, h, pixels); + img_okay = ImageBMP::Read(data, bytes, transparent, image_out); else if (bytes > 4 && strncmp((char*)(data + 1), "PNG", 3) == 0) - img_okay = ImagePNG::ReadPNG((const void*) data, transparent, w, h, pixels); + img_okay = ImagePNG::Read((const void*) data, transparent, image_out); else Output::Warning("Unsupported image (Magic: {:02X})", bytes >= 4 ? *reinterpret_cast(data) : 0); if (!img_okay) { - free(pixels); - pixels = nullptr; + free(image_out.pixels); return; } - Init(w, h, nullptr); + Init(image_out.width, image_out.height, nullptr); + + ConvertImage(image_out.width, image_out.height, image_out.pixels, transparent); - ConvertImage(w, h, pixels, transparent); + original_bpp = image_out.bpp; CheckPixels(flags); } @@ -183,7 +182,7 @@ bool Bitmap::WritePNG(std::ostream& os) const { pixman_image_composite32(PIXMAN_OP_SRC, bitmap.get(), NULL, dst.get(), 0, 0, 0, 0, 0, 0, width, height); - return ImagePNG::WritePNG(os, width, height, &data.front()); + return ImagePNG::Write(os, width, height, &data.front()); } size_t Bitmap::GetSize() const { diff --git a/src/bitmap.h b/src/bitmap.h index 3fbfc699d4..b7be3f1dbf 100644 --- a/src/bitmap.h +++ b/src/bitmap.h @@ -224,6 +224,13 @@ class Bitmap { */ StringView GetFilename() const; + /** + * Gets bpp of the source image. + * + * @return Bpp + */ + int GetOriginalBpp() const; + void CheckPixels(uint32_t flags); /** @@ -599,6 +606,9 @@ class Bitmap { std::string filename; + /** Bpp of the source image */ + int original_bpp; + /** Bitmap data. */ PixmanImagePtr bitmap; pixman_format_code_t pixman_format; @@ -636,6 +646,13 @@ class Bitmap { bool read_only = false; }; +struct ImageOut { + int width = 0; + int height = 0; + void* pixels = nullptr; + int bpp = 0; +}; + inline ImageOpacity Bitmap::GetImageOpacity() const { return image_opacity; } @@ -672,4 +689,8 @@ inline StringView Bitmap::GetFilename() const { return filename; } +inline int Bitmap::GetOriginalBpp() const { + return original_bpp; +} + #endif diff --git a/src/cache.cpp b/src/cache.cpp index bfa3dfbb29..5e55a868f7 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -35,6 +35,7 @@ #include "player.h" #include #include "game_clock.h" +#include "translation.h" using namespace std::chrono_literals; @@ -272,6 +273,14 @@ namespace { bmp = Bitmap::Create(std::move(is), transparent, flags); if (!bmp) { Output::Warning("Invalid image: {}/{}", s.directory, filename); + } else { + if (bmp->GetOriginalBpp() > 8) { + if (!Player::HasEasyRpgExtensions() && !Player::IsPatchManiac() && !Tr::HasActiveTranslation()) { + Output::Warning("Image {}/{} has a bit depth of {} that is not supported by RPG_RT. Enable EasyRPG Extensions or Maniac Patch to load such images.", s.directory, filename, bmp->GetOriginalBpp()); + } + + bmp.reset(); + } } } } diff --git a/src/game_character.cpp b/src/game_character.cpp index 8c9150db36..0619addb88 100644 --- a/src/game_character.cpp +++ b/src/game_character.cpp @@ -71,7 +71,7 @@ int Game_Character::GetJumpHeight() const { return 0; } -int Game_Character::GetScreenX(bool apply_shift) const { +int Game_Character::GetScreenX() const { int x = GetSpriteX() / TILE_SIZE - Game_Map::GetDisplayX() / TILE_SIZE + TILE_SIZE; if (Game_Map::LoopHorizontal()) { @@ -79,14 +79,10 @@ int Game_Character::GetScreenX(bool apply_shift) const { } x -= TILE_SIZE / 2; - if (apply_shift) { - x += Game_Map::GetTilesX() * TILE_SIZE; - } - return x; } -int Game_Character::GetScreenY(bool apply_shift, bool apply_jump) const { +int Game_Character::GetScreenY(bool apply_jump) const { int y = GetSpriteY() / TILE_SIZE - Game_Map::GetDisplayY() / TILE_SIZE + TILE_SIZE; if (apply_jump) { @@ -97,14 +93,10 @@ int Game_Character::GetScreenY(bool apply_shift, bool apply_jump) const { y = Utils::PositiveModulo(y, Game_Map::GetTilesY() * TILE_SIZE); } - if (apply_shift) { - y += Game_Map::GetTilesY() * TILE_SIZE; - } - return y; } -Drawable::Z_t Game_Character::GetScreenZ(bool apply_shift) const { +Drawable::Z_t Game_Character::GetScreenZ(int x_offset, int y_offset) const { Drawable::Z_t z = 0; if (IsFlying()) { @@ -118,8 +110,8 @@ Drawable::Z_t Game_Character::GetScreenZ(bool apply_shift) const { } // 0x8000 (32768) is added to shift negative numbers into the positive range - Drawable::Z_t y = static_cast(GetScreenY(apply_shift, false) + 0x8000); - Drawable::Z_t x = static_cast(GetScreenX(apply_shift) + 0x8000); + Drawable::Z_t y = static_cast(GetScreenY(false) + y_offset + 0x8000); + Drawable::Z_t x = static_cast(GetScreenX() + x_offset + 0x8000); // The rendering order of characters is: Highest Y-coordinate, Highest X-coordinate, Highest ID // To encode this behaviour all of them get 16 Bit in the Z value diff --git a/src/game_character.h b/src/game_character.h index 64dcf2e4d8..152b275bb3 100644 --- a/src/game_character.h +++ b/src/game_character.h @@ -693,27 +693,26 @@ class Game_Character { /** * Gets sprite x coordinate transformed to screen coordinate in pixels. * - * @param apply_shift When true the coordinate is shifted by the map width (for looping maps) * @return screen x coordinate in pixels. */ - virtual int GetScreenX(bool apply_shift = false) const; + virtual int GetScreenX() const; /** * Gets sprite y coordinate transformed to screen coordinate in pixels. * - * @param apply_shift When true the coordinate is shifted by the map height (for looping maps) * @param apply_jump Apply jump height modifier if character is jumping * @return screen y coordinate in pixels. */ - virtual int GetScreenY(bool apply_shift = false, bool apply_jump = true) const; + virtual int GetScreenY(bool apply_jump = true) const; /** * Gets screen z coordinate * - * @param apply_shift Forwarded to GetScreenY + * @param x_offset Offset to apply to the X coordinate + * @param y_offset Offset to apply to the Y coordinate * @return screen z coordinate */ - virtual Drawable::Z_t GetScreenZ(bool apply_shift = false) const; + virtual Drawable::Z_t GetScreenZ(int x_offset, int y_offset) const; /** * Gets tile graphic ID. diff --git a/src/game_config_game.cpp b/src/game_config_game.cpp index bc44224dd1..c297936e5f 100644 --- a/src/game_config_game.cpp +++ b/src/game_config_game.cpp @@ -89,13 +89,23 @@ void Game_ConfigGame::LoadFromArgs(CmdlineParser& cp) { patch_override = true; continue; } + if (cp.ParseNext(arg, 0, {"--patch-easyrpg", "--no-patch-easyrpg"})) { + patch_easyrpg.Set(arg.ArgIsOn()); + patch_override = true; + continue; + } if (cp.ParseNext(arg, 0, {"--patch-dynrpg", "--no-patch-dynrpg"})) { patch_dynrpg.Set(arg.ArgIsOn()); patch_override = true; continue; } - if (cp.ParseNext(arg, 0, {"--patch-maniac", "--no-patch-maniac"})) { + if (cp.ParseNext(arg, 1, {"--patch-maniac", "--no-patch-maniac"})) { patch_maniac.Set(arg.ArgIsOn()); + + if (arg.ArgIsOn() && arg.ParseValue(0, li_value)) { + patch_maniac.Set(li_value); + } + patch_override = true; continue; } @@ -170,6 +180,10 @@ void Game_ConfigGame::LoadFromStream(Filesystem_Stream::InputStream& is) { engine_str.FromIni(ini); fake_resolution.FromIni(ini); + if (patch_easyrpg.FromIni(ini)) { + patch_override = true; + } + if (patch_dynrpg.FromIni(ini)) { patch_override = true; } diff --git a/src/game_config_game.h b/src/game_config_game.h index c91d5d1e22..0106d67b28 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -38,8 +38,9 @@ struct Game_ConfigGame { BoolConfigParam new_game{ "Start new game", "Skips the title screen and starts a new game directly", "Game", "NewGame", false }; StringConfigParam engine_str{ "Engine", "", "Game", "Engine", std::string() }; BoolConfigParam fake_resolution{ "Fake Metrics", "Makes games run on higher resolutions (with some success)", "Game", "FakeResolution", false }; + BoolConfigParam patch_easyrpg{ "EasyRPG", "EasyRPG Engine Extensions", "Patch", "EasyRPG", false }; BoolConfigParam patch_dynrpg{ "DynRPG", "", "Patch", "DynRPG", false }; - BoolConfigParam patch_maniac{ "Maniac Patch", "", "Patch", "Maniac", false }; + ConfigParam patch_maniac{ "Maniac Patch", "", "Patch", "Maniac", 0 }; BoolConfigParam patch_common_this_event{ "Common This Event", "Support \"This Event\" in Common Events", "Patch", "CommonThisEvent", false }; BoolConfigParam patch_unlock_pics{ "Unlock Pictures", "Allow picture commands while a message is shown", "Patch", "PicUnlock", false }; BoolConfigParam patch_key_patch{ "Ineluki Key Patch", "Support \"Ineluki Key Patch\"", "Patch", "KeyPatch", false }; diff --git a/src/game_event.cpp b/src/game_event.cpp index 83d0be0c4b..858427119d 100644 --- a/src/game_event.cpp +++ b/src/game_event.cpp @@ -115,10 +115,10 @@ lcf::rpg::SaveMapEvent Game_Event::GetSaveData() const { return save; } -Drawable::Z_t Game_Event::GetScreenZ(bool apply_shift) const { +Drawable::Z_t Game_Event::GetScreenZ(int x_offset, int y_offset) const { // Lowest 16 bit are reserved for the ID // See base function for full explanation - return Game_Character::GetScreenZ(apply_shift) + GetId(); + return Game_Character::GetScreenZ(x_offset, y_offset) + GetId(); } int Game_Event::GetOriginalMoveRouteIndex() const { diff --git a/src/game_event.h b/src/game_event.h index 7b763adf59..b1dede55ba 100644 --- a/src/game_event.h +++ b/src/game_event.h @@ -49,7 +49,7 @@ class Game_Event : public Game_EventBase { * Implementation of abstract methods */ /** @{ */ - Drawable::Z_t GetScreenZ(bool apply_shift = false) const override; + Drawable::Z_t GetScreenZ(int x_offset, int y_offset) const override; bool Move(int dir) override; void UpdateNextMovementAction() override; bool IsVisible() const override; diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 88e5cde25a..7490ae105b 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -678,6 +678,10 @@ bool Game_Interpreter_Map::CommandToggleAtbMode(lcf::rpg::EventCommand const& /* } bool Game_Interpreter_Map::CommandEasyRpgTriggerEventAt(lcf::rpg::EventCommand const& com) { + if (!Player::HasEasyRpgExtensions()) { + return true; + } + int x = ValueOrVariable(com.parameters[0], com.parameters[1]); int y = ValueOrVariable(com.parameters[2], com.parameters[3]); diff --git a/src/game_player.cpp b/src/game_player.cpp index 71c8694ac1..c20a02a9ef 100644 --- a/src/game_player.cpp +++ b/src/game_player.cpp @@ -65,13 +65,13 @@ lcf::rpg::SavePartyLocation Game_Player::GetSaveData() const { return *data(); } -Drawable::Z_t Game_Player::GetScreenZ(bool apply_shift) const { +Drawable::Z_t Game_Player::GetScreenZ(int x_offset, int y_offset) const { // Player is always "same layer as hero". // When the Player is on the same Y-coordinate as an event the Player is always rendered first. // This is different to events where, when Y is the same, the highest X-coordinate is rendered first. // To ensure this, fake a very high X-coordinate of 65535 (all bits set) // See base function for full explanation of the bitmask - return Game_Character::GetScreenZ(apply_shift) | (0xFFFFu << 16u); + return Game_Character::GetScreenZ(x_offset, y_offset) | (0xFFFFu << 16u); } void Game_Player::ReserveTeleport(int map_id, int x, int y, int direction, TeleportTarget::Type tt) { diff --git a/src/game_player.h b/src/game_player.h index 79b24d8200..9cd826b074 100644 --- a/src/game_player.h +++ b/src/game_player.h @@ -47,7 +47,7 @@ class Game_Player : public Game_PlayerBase { * Implementation of abstract methods */ /** @{ */ - Drawable::Z_t GetScreenZ(bool apply_shift = false) const override; + Drawable::Z_t GetScreenZ(int x_offset, int y_offset) const override; bool IsVisible() const override; bool MakeWay(int from_x, int from_y, int to_x, int to_y) override; void UpdateNextMovementAction() override; diff --git a/src/game_strings.cpp b/src/game_strings.cpp index 1626dd709d..1258a854ea 100644 --- a/src/game_strings.cpp +++ b/src/game_strings.cpp @@ -25,6 +25,7 @@ #include "game_switches.h" #include "game_variables.h" #include "output.h" +#include "player.h" #include "utils.h" void Game_Strings::WarnGet(int id) const { @@ -186,7 +187,7 @@ StringView Game_Strings::ToFile(Str_Params params, std::string filename, int enc filename = "Text/" + filename; // EasyRPG Extension: When "*" is at the end of filename, ".txt" is not appended - if (filename.back() == '*') { + if (Player::HasEasyRpgExtensions() && filename.back() == '*') { filename.pop_back(); } else { filename += ".txt"; diff --git a/src/game_vehicle.cpp b/src/game_vehicle.cpp index 176e086fe1..50688c8ebb 100644 --- a/src/game_vehicle.cpp +++ b/src/game_vehicle.cpp @@ -147,8 +147,8 @@ int Game_Vehicle::GetAltitude() const { return SCREEN_TILE_SIZE / (SCREEN_TILE_SIZE / TILE_SIZE); } -int Game_Vehicle::GetScreenY(bool apply_shift, bool apply_jump) const { - return Game_Character::GetScreenY(apply_shift, apply_jump) - GetAltitude(); +int Game_Vehicle::GetScreenY(bool apply_jump) const { + return Game_Character::GetScreenY(apply_jump) - GetAltitude(); } bool Game_Vehicle::CanLand() const { diff --git a/src/game_vehicle.h b/src/game_vehicle.h index e4072a7ead..eb78c8a71b 100644 --- a/src/game_vehicle.h +++ b/src/game_vehicle.h @@ -72,7 +72,7 @@ class Game_Vehicle : public Game_VehicleBase { bool IsAboard() const; void SyncWithRider(const Game_Character* rider); bool AnimateAscentDescent(); - int GetScreenY(bool apply_shift = false, bool apply_jump = true) const override; + int GetScreenY(bool apply_jump = true) const override; bool CanLand() const; void StartAscent(); void StartDescent(); diff --git a/src/image_bmp.cpp b/src/image_bmp.cpp index fb8d67d6c2..2adf6fc903 100644 --- a/src/image_bmp.cpp +++ b/src/image_bmp.cpp @@ -84,9 +84,8 @@ ImageBMP::BitmapHeader ImageBMP::ParseHeader(const uint8_t*& ptr, uint8_t const* return hdr; } -bool ImageBMP::ReadBMP(const uint8_t* data, unsigned len, bool transparent, - int& width, int& height, void*& pixels) { - pixels = nullptr; +bool ImageBMP::Read(const uint8_t* data, unsigned len, bool transparent, ImageOut& output) { + output.pixels = nullptr; if (len < 64) { Output::Warning("Not a valid BMP file."); @@ -142,13 +141,13 @@ bool ImageBMP::ReadBMP(const uint8_t* data, unsigned len, bool transparent, int line_width = (hdr.depth == 4) ? (hdr.w + 1) >> 1 : hdr.w; int padding = (-line_width)&3; - pixels = malloc(hdr.w * hdr.h * 4); - if (!pixels) { + output.pixels = malloc(hdr.w * hdr.h * 4); + if (!output.pixels) { Output::Warning("Error allocating BMP pixel buffer."); return false; } - uint8_t* dst = (uint8_t*) pixels; + uint8_t* dst = (uint8_t*) output.pixels; for (int y = 0; y < hdr.h; y++) { const uint8_t* src = src_pixels + (vflip ? hdr.h - 1 - y : y) * (line_width + padding); for (int x = 0; x < hdr.w; x += 2) { @@ -182,13 +181,13 @@ bool ImageBMP::ReadBMP(const uint8_t* data, unsigned len, bool transparent, } } - width = hdr.w; - height = hdr.h; + output.width = hdr.w; + output.height = hdr.h; + output.bpp = hdr.depth; // Currently only 4 and 8 bit (indexed) are supported return true; } -bool ImageBMP::ReadBMP(Filesystem_Stream::InputStream& stream, bool transparent, - int& width, int& height, void*& pixels) { +bool ImageBMP::Read(Filesystem_Stream::InputStream& stream, bool transparent, ImageOut& output) { std::vector buffer = Utils::ReadStream(stream); - return ReadBMP(&buffer.front(), (unsigned) buffer.size(), transparent, width, height, pixels); + return Read(&buffer.front(), (unsigned) buffer.size(), transparent, output); } diff --git a/src/image_bmp.h b/src/image_bmp.h index 9c449bf0cf..85979befa1 100644 --- a/src/image_bmp.h +++ b/src/image_bmp.h @@ -19,6 +19,7 @@ #define EP_IMAGE_BMP_H #include +#include "bitmap.h" #include "filesystem_stream.h" namespace ImageBMP { @@ -33,8 +34,8 @@ namespace ImageBMP { int palette_size = 0; }; - bool ReadBMP(const uint8_t* data, unsigned len, bool transparent, int& width, int& height, void*& pixels); - bool ReadBMP(Filesystem_Stream::InputStream& stream, bool transparent, int& width, int& height, void*& pixels); + bool Read(const uint8_t* data, unsigned len, bool transparent, ImageOut& output); + bool Read(Filesystem_Stream::InputStream& stream, bool transparent, ImageOut& output); BitmapHeader ParseHeader(const uint8_t*& ptr, uint8_t const* e); } diff --git a/src/image_png.cpp b/src/image_png.cpp index 1862416616..4eaddcb613 100644 --- a/src/image_png.cpp +++ b/src/image_png.cpp @@ -47,26 +47,23 @@ static void on_png_error(png_structp, png_const_charp error_msg) { Output::Warning("libpng: {}", error_msg); } -static bool ReadPNGWithReadFunction(png_voidp,png_rw_ptr, bool, int&, int&, void*&); +static bool ReadPNGWithReadFunction(png_voidp,png_rw_ptr, bool, ImageOut&); static void ReadPalettedData(png_struct*, png_info*, png_uint_32, png_uint_32, bool, uint32_t*); static void ReadGrayData(png_struct*, png_info*, png_uint_32, png_uint_32, bool, uint32_t*); static void ReadGrayAlphaData(png_struct*, png_info*, png_uint_32, png_uint_32, uint32_t*); static void ReadRGBData(png_struct*, png_info*, png_uint_32, png_uint_32, uint32_t*); static void ReadRGBAData(png_struct*, png_info*, png_uint_32, png_uint_32, uint32_t*); -bool ImagePNG::ReadPNG(const void* buffer, bool transparent, - int& width, int& height, void*& pixels) { - return ReadPNGWithReadFunction((png_voidp)&buffer, read_data, transparent, width, height, pixels); +bool ImagePNG::Read(const void* buffer, bool transparent, ImageOut& output) { + return ReadPNGWithReadFunction((png_voidp)&buffer, read_data, transparent, output); } -bool ImagePNG::ReadPNG(Filesystem_Stream::InputStream& stream, bool transparent, - int& width, int& height, void*& pixels) { - return ReadPNGWithReadFunction(&stream, read_data_istream, transparent, width, height, pixels); +bool ImagePNG::Read(Filesystem_Stream::InputStream& stream, bool transparent, ImageOut& output) { + return ReadPNGWithReadFunction(&stream, read_data_istream, transparent, output); } -static bool ReadPNGWithReadFunction(png_voidp user_data, png_rw_ptr fn, bool transparent, - int& width, int& height, void*& pixels) { - pixels = nullptr; +static bool ReadPNGWithReadFunction(png_voidp user_data, png_rw_ptr fn, bool transparent, ImageOut& output) { + output.pixels = nullptr; png_struct *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, on_png_error, on_png_warning); if (png_ptr == NULL) { @@ -94,35 +91,40 @@ static bool ReadPNGWithReadFunction(png_voidp user_data, png_rw_ptr fn, bool tra png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &color_type, NULL, NULL, NULL); - pixels = malloc(w * h * 4); - if (!pixels) { + output.pixels = malloc(w * h * 4); + if (!output.pixels) { Output::Warning("Error allocating PNG pixel buffer."); return false; } switch (color_type) { case PNG_COLOR_TYPE_PALETTE: - ReadPalettedData(png_ptr, info_ptr, w, h, transparent, (uint32_t*)pixels); + ReadPalettedData(png_ptr, info_ptr, w, h, transparent, (uint32_t*)output.pixels); + output.bpp = 8; break; case PNG_COLOR_TYPE_GRAY: - ReadGrayData(png_ptr, info_ptr, w, h, transparent, (uint32_t*)pixels); + ReadGrayData(png_ptr, info_ptr, w, h, transparent, (uint32_t*)output.pixels); + output.bpp = 8; break; case PNG_COLOR_TYPE_GRAY_ALPHA: - ReadGrayAlphaData(png_ptr, info_ptr, w, h, (uint32_t*)pixels); + ReadGrayAlphaData(png_ptr, info_ptr, w, h, (uint32_t*)output.pixels); + output.bpp = 8; break; case PNG_COLOR_TYPE_RGB: - ReadRGBData(png_ptr, info_ptr, w, h, (uint32_t*)pixels); + ReadRGBData(png_ptr, info_ptr, w, h, (uint32_t*)output.pixels); + output.bpp = 24; break; case PNG_COLOR_TYPE_RGB_ALPHA: - ReadRGBAData(png_ptr, info_ptr, w, h, (uint32_t*)pixels); + ReadRGBAData(png_ptr, info_ptr, w, h, (uint32_t*)output.pixels); + output.bpp = 32; break; } png_read_end(png_ptr, NULL); png_destroy_read_struct(&png_ptr, &info_ptr, NULL); - width = w; - height = h; + output.width = w; + output.height = h; return true; } @@ -251,7 +253,7 @@ static void flush_stream(png_structp out_ptr) { reinterpret_cast(png_get_io_ptr(out_ptr))->flush(); } -bool ImagePNG::WritePNG(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data) { +bool ImagePNG::Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data) { png_structp write = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!write) { Output::Warning("Bitmap::WritePNG: error in png_create_write"); diff --git a/src/image_png.h b/src/image_png.h index f81dd9496c..d8ffd112f2 100644 --- a/src/image_png.h +++ b/src/image_png.h @@ -19,12 +19,13 @@ #define EP_IMAGE_PNG_H #include +#include "bitmap.h" #include "filesystem_stream.h" namespace ImagePNG { - bool ReadPNG(const void* buffer, bool transparent, int& width, int& height, void*& pixels); - bool ReadPNG(Filesystem_Stream::InputStream& is, bool transparent, int& width, int& height, void*& pixels); - bool WritePNG(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data); + bool Read(const void* buffer, bool transparent, ImageOut& output); + bool Read(Filesystem_Stream::InputStream& is, bool transparent, ImageOut& output); + bool Write(std::ostream& os, uint32_t width, uint32_t height, uint32_t* data); } #endif diff --git a/src/image_xyz.cpp b/src/image_xyz.cpp index 5a9fa8575b..26d1a5cdad 100644 --- a/src/image_xyz.cpp +++ b/src/image_xyz.cpp @@ -24,9 +24,8 @@ #include "output.h" #include "image_xyz.h" -bool ImageXYZ::ReadXYZ(const uint8_t* data, unsigned len, bool transparent, - int& width, int& height, void*& pixels) { - pixels = nullptr; +bool ImageXYZ::Read(const uint8_t* data, unsigned len, bool transparent, ImageOut& output) { + output.pixels = nullptr; if (len < 8) { Output::Warning("Not a valid XYZ file."); @@ -47,13 +46,13 @@ bool ImageXYZ::ReadXYZ(const uint8_t* data, unsigned len, bool transparent, } const uint8_t (*palette)[3] = (const uint8_t(*)[3]) &dst_buffer.front(); - pixels = malloc(w * h * 4); - if (!pixels) { + output.pixels = malloc(w * h * 4); + if (!output.pixels) { Output::Warning("Error allocating XYZ pixel buffer."); return false; } - uint8_t* dst = (uint8_t*) pixels; + uint8_t* dst = (uint8_t*) output.pixels; const uint8_t* src = (const uint8_t*) &dst_buffer[768]; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { @@ -66,13 +65,14 @@ bool ImageXYZ::ReadXYZ(const uint8_t* data, unsigned len, bool transparent, } } - width = w; - height = h; + output.width = w; + output.height = h; + output.bpp = 8; + return true; } -bool ImageXYZ::ReadXYZ(Filesystem_Stream::InputStream& stream, bool transparent, - int& width, int& height, void*& pixels) { +bool ImageXYZ::Read(Filesystem_Stream::InputStream& stream, bool transparent, ImageOut& output) { std::vector buffer = Utils::ReadStream(stream); - return ReadXYZ(&buffer.front(), (unsigned) buffer.size(), transparent, width, height, pixels); + return Read(&buffer.front(), (unsigned) buffer.size(), transparent, output); } diff --git a/src/image_xyz.h b/src/image_xyz.h index 9890b22bc6..bc80f6ed99 100644 --- a/src/image_xyz.h +++ b/src/image_xyz.h @@ -20,11 +20,12 @@ #include #include +#include "bitmap.h" #include "filesystem_stream.h" namespace ImageXYZ { - bool ReadXYZ(const uint8_t* data, unsigned len, bool transparent, int& width, int& height, void*& pixels); - bool ReadXYZ(Filesystem_Stream::InputStream& stream, bool transparent, int& width, int& height, void*& pixels); + bool Read(const uint8_t* data, unsigned len, bool transparent, ImageOut& output); + bool Read(Filesystem_Stream::InputStream& stream, bool transparent, ImageOut& output); } #endif diff --git a/src/player.cpp b/src/player.cpp index ac36d0b7d8..46e20d9de9 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -825,8 +825,9 @@ void Player::CreateGameObjects() { } } - Output::Debug("Patch configuration: dynrpg={} maniac={} key-patch={} common-this={} pic-unlock={} 2k3-commands={} anti-lag-switch={}", - Player::IsPatchDynRpg(), Player::IsPatchManiac(), Player::IsPatchKeyPatch(), game_config.patch_common_this_event.Get(), game_config.patch_unlock_pics.Get(), game_config.patch_rpg2k3_commands.Get(), game_config.patch_anti_lag_switch.Get()); + Output::Debug("Patch configuration: easyrpg={} dynrpg={} maniac={} key-patch={} common-this={} pic-unlock={} 2k3-commands={} anti-lag-switch={}", + Player::HasEasyRpgExtensions(), Player::IsPatchDynRpg(), Player::IsPatchManiac(), Player::IsPatchKeyPatch(), game_config.patch_common_this_event.Get(), + game_config.patch_unlock_pics.Get(), game_config.patch_rpg2k3_commands.Get(), game_config.patch_anti_lag_switch.Get()); ResetGameObjects(); @@ -879,7 +880,7 @@ void Player::ResetGameObjects() { auto min_var = lcf::Data::system.easyrpg_variable_min_value; if (min_var == 0) { - if (Player::IsPatchManiac()) { + if ((Player::game_config.patch_maniac.Get() & 1) == 1) { min_var = std::numeric_limits::min(); } else { min_var = Player::IsRPG2k3() ? Game_Variables::min_2k3 : Game_Variables::min_2k; @@ -887,7 +888,7 @@ void Player::ResetGameObjects() { } auto max_var = lcf::Data::system.easyrpg_variable_max_value; if (max_var == 0) { - if (Player::IsPatchManiac()) { + if ((Player::game_config.patch_maniac.Get() & 1) == 1) { max_var = std::numeric_limits::max(); } else { max_var = Player::IsRPG2k3() ? Game_Variables::max_2k3 : Game_Variables::max_2k; @@ -1399,8 +1400,12 @@ Engine options: --patch-common-this Enable usage of "This Event" in common events in any version of the engine. --patch-dynrpg Enable support of DynRPG patch by Cherry (very limited). + --patch-easyrpg Enable EasyRPG extensions. --patch-key-patch Enable Key Patch by Ineluki. - --patch-maniac Enable Maniac Patch by BingShan. + --patch-maniac [N] Enable Maniac Patch by BingShan. Values for N: + - 1: Enable the patch (default) + - 2: Enable the patch but do not adjust variable ranges + to 32 bit. --patch-pic-unlock Picture movement is not interrupted by messages in any version of the engine. --patch-rpg2k3-cmds Support all RPG Maker 2003 event commands in any version @@ -1549,4 +1554,3 @@ std::string Player::GetEngineVersion() { if (EngineVersion() > 0) return std::to_string(EngineVersion()); return std::string(); } - diff --git a/src/player.h b/src/player.h index dc7dd13b0e..bf220f91fc 100644 --- a/src/player.h +++ b/src/player.h @@ -292,6 +292,11 @@ namespace Player { */ bool IsPatchKeyPatch(); + /** + * @return True when EasyRpg extensions are on + */ + bool HasEasyRpgExtensions(); + /** * @return Running engine version. 2000 for RPG2k and 2003 for RPG2k3 */ @@ -473,11 +478,15 @@ inline bool Player::IsPatchDynRpg() { } inline bool Player::IsPatchManiac() { - return game_config.patch_maniac.Get(); + return game_config.patch_maniac.Get() > 0; } inline bool Player::IsPatchKeyPatch() { return game_config.patch_key_patch.Get(); } +inline bool Player::HasEasyRpgExtensions() { + return game_config.patch_easyrpg.Get(); +} + #endif diff --git a/src/sprite_airshipshadow.cpp b/src/sprite_airshipshadow.cpp index 15a0da95bf..ce51f8230f 100644 --- a/src/sprite_airshipshadow.cpp +++ b/src/sprite_airshipshadow.cpp @@ -25,16 +25,14 @@ #include "sprite_airshipshadow.h" #include -Sprite_AirshipShadow::Sprite_AirshipShadow(CloneType type) { +Sprite_AirshipShadow::Sprite_AirshipShadow(int x_offset, int y_offset) : + x_offset(x_offset), y_offset(y_offset) { SetBitmap(Bitmap::Create(16,16)); SetOx(TILE_SIZE/2); SetOy(TILE_SIZE); RecreateShadow(); - - x_shift = ((type & XClone) == XClone); - y_shift = ((type & YClone) == YClone); } // Draws the two shadow sprites to a single intermediate bitmap to be blit to the map @@ -56,6 +54,19 @@ void Sprite_AirshipShadow::RecreateShadow() { GetBitmap()->Blit(0, 0, *system, Rect(128+16,32,16,16), opacity); } +void Sprite_AirshipShadow::Draw(Bitmap &dst) { + Game_Vehicle* airship = Game_Map::GetVehicle(Game_Vehicle::Airship); + const int altitude = airship->GetAltitude(); + const int max_altitude = TILE_SIZE; + const double opacity = (double)altitude / max_altitude; + SetOpacity(opacity * 255); + + SetX(Main_Data::game_player->GetScreenX() + x_offset); + SetY(Main_Data::game_player->GetScreenY() + y_offset + Main_Data::game_player->GetJumpHeight()); + + Sprite::Draw(dst); +} + void Sprite_AirshipShadow::Update() { if (!Main_Data::game_player->InAirship()) { SetVisible(false); @@ -65,13 +76,6 @@ void Sprite_AirshipShadow::Update() { Game_Vehicle* airship = Game_Map::GetVehicle(Game_Vehicle::Airship); - const int altitude = airship->GetAltitude(); - const int max_altitude = TILE_SIZE; - const double opacity = (double)altitude / max_altitude; - SetOpacity(opacity * 255); - - SetX(Main_Data::game_player->GetScreenX(x_shift)); - SetY(Main_Data::game_player->GetScreenY(y_shift) + Main_Data::game_player->GetJumpHeight()); // Synchronized with airship priority - SetZ(airship->GetScreenZ(y_shift) - 1); + SetZ(airship->GetScreenZ(x_offset, y_offset) - 1); } diff --git a/src/sprite_airshipshadow.h b/src/sprite_airshipshadow.h index adf689c9a8..f732268646 100644 --- a/src/sprite_airshipshadow.h +++ b/src/sprite_airshipshadow.h @@ -37,13 +37,14 @@ class Sprite_AirshipShadow : public Sprite { YClone = 4 }; - Sprite_AirshipShadow(CloneType type = CloneType::Original); + Sprite_AirshipShadow(int x_offset = 0, int y_offset = 0); + void Draw(Bitmap& dst) override; void Update(); void RecreateShadow(); private: - bool x_shift = false; - bool y_shift = false; + int x_offset = 0; + int y_offset = 0; }; #endif diff --git a/src/sprite_character.cpp b/src/sprite_character.cpp index d291b5ad5d..f0dccc4975 100644 --- a/src/sprite_character.cpp +++ b/src/sprite_character.cpp @@ -20,20 +20,41 @@ #include "cache.h" #include "game_map.h" #include "bitmap.h" +#include "output.h" -Sprite_Character::Sprite_Character(Game_Character* character, CloneType type) : +Sprite_Character::Sprite_Character(Game_Character* character, int x_offset, int y_offset) : character(character), tile_id(-1), character_index(0), chara_width(0), - chara_height(0) { - - x_shift = ((type & XClone) == XClone); - y_shift = ((type & YClone) == YClone); + chara_height(0), + x_offset(x_offset), + y_offset(y_offset) { Update(); } +void Sprite_Character::Draw(Bitmap &dst) { + if (UsesCharset()) { + int row = character->GetFacing(); + auto frame = character->GetAnimFrame(); + if (frame >= lcf::rpg::EventPage::Frame_middle2) frame = lcf::rpg::EventPage::Frame_middle; + SetSrcRect({frame * chara_width, row * chara_height, chara_width, chara_height}); + } + + SetFlashEffect(character->GetFlashColor()); + + SetOpacity(character->GetOpacity()); + + SetX(character->GetScreenX() + x_offset); + SetY(character->GetScreenY() + y_offset); + + int bush_split = 4 - character->GetBushDepth(); + SetBushDepth(bush_split > 3 ? 0 : GetHeight() / bush_split); + + Sprite::Draw(dst); +} + void Sprite_Character::Update() { if (tile_id != character->GetTileId() || character_name != character->GetSpriteName() || @@ -63,30 +84,14 @@ void Sprite_Character::Update() { } } - if (UsesCharset()) { - int row = character->GetFacing(); - auto frame = character->GetAnimFrame(); - if (frame >= lcf::rpg::EventPage::Frame_middle2) frame = lcf::rpg::EventPage::Frame_middle; - SetSrcRect({frame * chara_width, row * chara_height, chara_width, chara_height}); - } - - SetFlashEffect(character->GetFlashColor()); - - SetOpacity(character->GetOpacity()); SetVisible(character->IsVisible()); - - SetX(character->GetScreenX(x_shift)); - SetY(character->GetScreenY(y_shift)); - // y_shift because Z is calculated via the screen Y position - SetZ(character->GetScreenZ(y_shift)); - - int bush_split = 4 - character->GetBushDepth(); - SetBushDepth(bush_split > 3 ? 0 : GetHeight() / bush_split); + SetZ(character->GetScreenZ(x_offset, y_offset)); } Game_Character* Sprite_Character::GetCharacter() { return character; } + void Sprite_Character::SetCharacter(Game_Character* new_character) { character = new_character; } @@ -124,16 +129,20 @@ void Sprite_Character::ChipsetUpdated() { Rect Sprite_Character::GetCharacterRect(StringView name, int index, const Rect bitmap_rect) { Rect rect; + rect.width = 24 * (TILE_SIZE / 16) * 3; + rect.height = 32 * (TILE_SIZE / 16) * 4; + // Allow large 4x2 spriteset of 3x4 sprites // when the character name starts with a $ sign. // This is not exactly the VX Ace way because // VX Ace uses a single 1x1 spriteset of 3x4 sprites. if (!name.empty() && name.front() == '$') { - rect.width = bitmap_rect.width * (TILE_SIZE / 16) / 4; - rect.height = bitmap_rect.height * (TILE_SIZE / 16) / 2; - } else { - rect.width = 24 * (TILE_SIZE / 16) * 3;; - rect.height = 32 * (TILE_SIZE / 16) * 4; + if (!Player::HasEasyRpgExtensions()) { + Output::Debug("Ignoring large charset {}. EasyRPG Extension not enabled.", name); + } else { + rect.width = bitmap_rect.width * (TILE_SIZE / 16) / 4; + rect.height = bitmap_rect.height * (TILE_SIZE / 16) / 2; + } } rect.x = (index % 4) * rect.width; rect.y = (index / 4) * rect.height; diff --git a/src/sprite_character.h b/src/sprite_character.h index 16920c48da..bb8f9187df 100644 --- a/src/sprite_character.h +++ b/src/sprite_character.h @@ -42,9 +42,12 @@ class Sprite_Character : public Sprite { * Constructor. * * @param character game character to display - * @param type Type of the sprite for multiple renderings on looping maps + * @param x_offset X Render offset when being a clone + * @param y_offset Y Render offset when being a clone */ - Sprite_Character(Game_Character* character, CloneType type = CloneType::Original); + Sprite_Character(Game_Character* character, int x_offset = 0, int y_offset = 0); + + void Draw(Bitmap& dst) override; /** * Updates sprite state. @@ -94,8 +97,8 @@ class Sprite_Character : public Sprite { /** Returns true for charset sprites; false for tiles. */ bool UsesCharset() const; - bool x_shift = false; - bool y_shift = false; + int x_offset = 0; + int y_offset = 0; bool refresh_bitmap = false; void OnTileSpriteReady(FileRequestResult*); diff --git a/src/spriteset_map.cpp b/src/spriteset_map.cpp index 4d19c24831..0d16a5277e 100644 --- a/src/spriteset_map.cpp +++ b/src/spriteset_map.cpp @@ -218,14 +218,18 @@ void Spriteset_Map::CreateSprite(Game_Character* character, bool create_x_clone, add_sprite(std::make_unique(character)); if (create_x_clone) { - add_sprite(std::make_unique(character, CloneType::XClone)); + add_sprite(std::make_unique(character, -map_tiles_x, 0)); + add_sprite(std::make_unique(character, map_tiles_x, 0)); } if (create_y_clone) { - add_sprite(std::make_unique(character, CloneType::YClone)); + add_sprite(std::make_unique(character, 0, -map_tiles_y)); + add_sprite(std::make_unique(character, 0, map_tiles_y)); } if (create_x_clone && create_y_clone) { - add_sprite(std::make_unique(character, - (CloneType)(CloneType::XClone | CloneType::YClone))); + add_sprite(std::make_unique(character, map_tiles_x, map_tiles_y)); + add_sprite(std::make_unique(character, -map_tiles_x, map_tiles_y)); + add_sprite(std::make_unique(character, map_tiles_x, -map_tiles_y)); + add_sprite(std::make_unique(character, -map_tiles_x, -map_tiles_y)); } } @@ -240,14 +244,18 @@ void Spriteset_Map::CreateAirshipShadowSprite(bool create_x_clone, bool create_y add_sprite(std::make_unique()); if (create_x_clone) { - add_sprite(std::make_unique(CloneType::XClone)); + add_sprite(std::make_unique(-map_tiles_x, 0)); + add_sprite(std::make_unique(map_tiles_x, 0)); } if (create_y_clone) { - add_sprite(std::make_unique(CloneType::YClone)); + add_sprite(std::make_unique(0, -map_tiles_y)); + add_sprite(std::make_unique(0, map_tiles_y)); } if (create_x_clone && create_y_clone) { - add_sprite(std::make_unique( - (CloneType)(CloneType::XClone | CloneType::YClone))); + add_sprite(std::make_unique(map_tiles_x, map_tiles_y)); + add_sprite(std::make_unique(-map_tiles_x, map_tiles_y)); + add_sprite(std::make_unique(map_tiles_x, -map_tiles_y)); + add_sprite(std::make_unique(-map_tiles_x, -map_tiles_y)); } } @@ -277,8 +285,10 @@ void Spriteset_Map::OnPanoramaSpriteReady(FileRequestResult* result) { void Spriteset_Map::CalculateMapRenderOffset() { map_render_ox = 0; map_render_oy = 0; - map_tiles_x = 0; - map_tiles_y = 0; + + // Smallest possible map. Smaller maps are hacked + map_tiles_x = std::max(Game_Map::GetTilesX(), 20) * TILE_SIZE; + map_tiles_y = std::max(Game_Map::GetTilesY(), 15) * TILE_SIZE; panorama->SetRenderOx(0); panorama->SetRenderOy(0); @@ -286,7 +296,6 @@ void Spriteset_Map::CalculateMapRenderOffset() { if (Player::game_config.fake_resolution.Get()) { // Resolution hack for tiles and sprites - // Smallest possible map. Smaller maps are hacked map_tiles_x = std::max(Game_Map::GetTilesX(), 20) * TILE_SIZE; map_tiles_y = std::max(Game_Map::GetTilesY(), 15) * TILE_SIZE; diff --git a/tests/cmdline_parser.cpp b/tests/cmdline_parser.cpp index 2ce378cfa3..faff3823b8 100644 --- a/tests/cmdline_parser.cpp +++ b/tests/cmdline_parser.cpp @@ -69,6 +69,23 @@ TEST_CASE("ParseMulti") { REQUIRE_EQ(li, 2); } +TEST_CASE("Parse Optional Value") { + std::vector args = { "testapp", "--arg1", "--arg2", "1", "--arg3", "a", "b" }; + + CmdlineParser cp(args); + + CmdlineArg arg; + + REQUIRE(cp.ParseNext(arg, 1, "--arg1")); + REQUIRE(arg.NumValues() == 0); + + REQUIRE(cp.ParseNext(arg, 2, "--arg2")); + REQUIRE(arg.NumValues() == 1); + + REQUIRE(cp.ParseNext(arg, 3, "--arg3")); + REQUIRE(arg.NumValues() == 2); +} + TEST_CASE("ParseNull") { std::vector args; CmdlineParser cp(args); diff --git a/tests/game_character.cpp b/tests/game_character.cpp index 99c6bc9bb7..09f0ab281f 100644 --- a/tests/game_character.cpp +++ b/tests/game_character.cpp @@ -77,20 +77,6 @@ static_assert(Game_Character::IsDirectionFixedAnimationType(lcf::rpg::EventPage: static_assert(!Game_Character::IsDirectionFixedAnimationType(lcf::rpg::EventPage::AnimType_spin), "DirFixedBroken"); static_assert(!Game_Character::IsDirectionFixedAnimationType(lcf::rpg::EventPage::AnimType_step_frame_fix), "DirFixedBroken"); -#if 0 - - /** @return the direction we would need to face the hero. */ - int GetDirectionToHero(); - - /** @return the direction we would need to face away from hero. */ - int GetDirectionAwayHero(); - - virtual Drawable::Z_t GetScreenZ(bool apply_shift = false) const; - - int DistanceXfromPlayer() const; - int DistanceYfromPlayer() const; -#endif - TEST_SUITE_BEGIN("Game_Character"); static void testInit(Game_Character& ch, int max_stop_count = 0) {