Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bitmap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Bitmap::Bitmap(int width, int height, bool transparent) {
Bitmap::Bitmap(void *pixels, int width, int height, int pitch, const DynamicFormat& _format) {
format = _format;
pixman_format = find_format(format);
Init(width, height, pixels, pitch, false);
Init(width, height, pixels, pitch, pixels == nullptr);
}

Bitmap::Bitmap(Filesystem_Stream::InputStream stream, bool transparent, uint32_t flags) {
Expand Down
3 changes: 2 additions & 1 deletion src/bitmap.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ class Bitmap {
static BitmapRef Create(int width, int height, bool transparent = true, int bpp = 0);

/**
* Creates a surface wrapper around existing pixel data.
* Creates a surface wrapper around pixel data.
* When the pixel data is NULL the data is allocated and managed by the bitmap.
*
* @param pixels pointer to pixel data.
* @param width surface width.
Expand Down
11 changes: 11 additions & 0 deletions src/cache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ namespace {
BitmapRef bmp;

const auto key = MakeHashKey(s.directory, filename, transparent, extra_flags);

auto it = cache.find(key);
if (it == cache.end()) {
if (filename == CACHE_DEFAULT_BITMAP) {
Expand Down Expand Up @@ -535,6 +536,16 @@ BitmapRef Cache::SpriteEffect(const BitmapRef& src_bitmap, const Rect& rect, boo
} else { return it->second.lock(); }
}

void Cache::Invalidate(std::string_view section) {
for (auto it = cache.begin(); it != cache.end(); ) {
if (StartsWith(it->first, section)) {
it = cache.erase(it);
} else {
++it;
}
}
}

void Cache::Clear() {
cache_effects.clear();
cache.clear();
Expand Down
6 changes: 6 additions & 0 deletions src/cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ namespace Cache {
BitmapRef Tile(std::string_view filename, int tile_id);
BitmapRef SpriteEffect(const BitmapRef& src_bitmap, const Rect& rect, bool flip_x, bool flip_y, const Tone& tone, const Color& blend);

/**
* Removes all cached entries of the given section
*
* @param section Cache section to remove
*/
void Invalidate(std::string_view section);
void Clear();
void ClearAll();

Expand Down
19 changes: 19 additions & 0 deletions src/filefinder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,25 @@ Filesystem_Stream::InputStream FileFinder::OpenText(std::string_view name) {
return open_generic_with_fallback("Text", name, args);
}

Filesystem_Stream::OutputStream FileFinder::OpenWrite(std::string_view name) {
std::string orig_name = FileFinder::MakeCanonical(name, 1);

std::string filename = FileFinder::Save().FindFile(name);

if (filename.empty()) {
// File not found: Create directory hierarchy to ensure file creation succeeds
auto dir = FileFinder::GetPathAndFilename(orig_name).first;

if (!dir.empty() && !FileFinder::Save().MakeDirectory(dir, false)) {
return Filesystem_Stream::OutputStream();
}

filename = orig_name;
}

return FileFinder::Save().OpenOutputStream(filename);
}

bool FileFinder::IsMajorUpdatedTree() {
auto fs = Game();
assert(fs);
Expand Down
11 changes: 11 additions & 0 deletions src/filefinder.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <cstdio>
#include <ios>
#include <istream>
#include <string_view>
#include <unordered_map>
#include <vector>

Expand Down Expand Up @@ -229,6 +230,16 @@ namespace FileFinder {
*/
Filesystem_Stream::InputStream OpenText(std::string_view name);

/**
* Opens a given file for writing in the save directory.
* Sanitizes the path and creates the directory hierarchy to the file when
* necessary.
*
* @param name
* @return Filesystem_Stream::OutputStream
*/
Filesystem_Stream::OutputStream OpenWrite(std::string_view name);

/**
* Appends name to directory.
*
Expand Down
178 changes: 162 additions & 16 deletions src/game_interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@
#include "game_interpreter_control_variables.h"
#include "game_windows.h"
#include "json_helper.h"
#include "lcf/rpg/savepicture.h"
#include "maniac_patch.h"
#include "memory_management.h"
#include "pixel_format.h"
#include "spriteset_map.h"
#include "sprite_character.h"
#include "scene_gameover.h"
Expand All @@ -70,6 +73,8 @@
#include "transition.h"
#include "baseui.h"
#include "algo.h"
#include "sprite_picture.h"
#include "bitmap.h"

using namespace Game_Interpreter_Shared;

Expand Down Expand Up @@ -809,14 +814,16 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) {
return CmdSetup<&Game_Interpreter::CommandManiacChangePictureId, 6>(com);
case Cmd::Maniac_SetGameOption:
return CmdSetup<&Game_Interpreter::CommandManiacSetGameOption, 4>(com);
case Cmd::Maniac_ControlStrings:
return CmdSetup<&Game_Interpreter::CommandManiacControlStrings, 8>(com);
case Cmd::Maniac_WritePicture:
return CmdSetup<&Game_Interpreter::CommandManiacWritePicture, 5>(com);
case Cmd::Maniac_CallCommand:
return CmdSetup<&Game_Interpreter::CommandManiacCallCommand, 6>(com);
case Cmd::Maniac_ControlStrings:
return CmdSetup<&Game_Interpreter::CommandManiacControlStrings, 8>(com);
case Cmd::Maniac_GetGameInfo:
return CmdSetup<&Game_Interpreter::CommandManiacGetGameInfo, 8>(com);
case Cmd::Maniac_EditPicture:
return CmdSetup<&Game_Interpreter::CommandManiacEditPicture, 8>(com);
case Cmd::Maniac_WritePicture:
return CmdSetup<&Game_Interpreter::CommandManiacWritePicture, 5>(com);
case Cmd::EasyRpg_SetInterpreterFlag:
return CmdSetup<&Game_Interpreter::CommandEasyRpgSetInterpreterFlag, 2>(com);
case Cmd::EasyRpg_ProcessJson:
Expand Down Expand Up @@ -4229,9 +4236,27 @@ bool Game_Interpreter::CommandManiacGetGameInfo(lcf::rpg::EventCommand const& co
Main_Data::game_variables->Set(var + 1, Player::screen_height);
break;
case 3: // Get pixel info
// FIXME: figure out how 'Pixel info' works
Output::Warning("GetGameInfo: Option 'Pixel Info' not implemented.");
{
// [0] Packing: x pos, y pos, width, height
int pic_x = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]);
int pic_y = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]);
int pic_w = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]);
int pic_h = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]);
int dst_var_id = com.parameters[7];

// Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB)
bool ignore_alpha = (com.parameters[2] & 1) != 0;

// Creates a snapshot of the current frame
BitmapRef screen = DisplayUi->CaptureScreen();
Rect frame_rect{pic_x, pic_y, pic_w, pic_h};

if (!ManiacPatch::WritePixelsToVariable(*screen, frame_rect, dst_var_id, ignore_alpha, *Main_Data::game_variables)) {
return true;
}

break;
}
case 4: // Get command interpreter state
{
// Parameter "Nest" in the English version of Maniacs
Expand Down Expand Up @@ -4704,7 +4729,8 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const&
return true;
}

int pic_id = ValueOrVariable(com.parameters[0], com.parameters[3]);
int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[3]);

auto& pic = Main_Data::game_pictures->GetPicture(pic_id);

if (pic.IsRequestPending()) {
Expand All @@ -4715,16 +4741,55 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const&
}

const auto& data = pic.data;
int info_type = com.parameters[1];

// Type 3: Pixel Data Extraction
if (info_type == 3) {
auto* sprite = pic.sprite.get();
auto bitmap = sprite->GetBitmap();

// If this is a Window (String Picture), the visual content is generated by the Window class.
// We force a refresh/draw cycle here to ensure we read the actual text/window graphics.
if (pic.IsWindowAttached()) {
const auto& window = Main_Data::game_windows->GetWindow(pic_id);
if (window.window) {
bitmap->Clear();
window.window->Draw(*bitmap);
}
}

// Packing: x pos, y pos, width, height, var_id
int pic_x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[4]);
int pic_y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[5]);
int pic_w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[6]);
int pic_h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[7]);
int dst_var_id = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[8]);

// Bit 0: Ignore Alpha (return 0x00RRGGBB instead of 0xFFRRGGBB)
bool ignore_alpha = (com.parameters[2] & 2) != 0;

Rect frame_rect{pic_x, pic_y, pic_w, pic_h};

if (!ManiacPatch::WritePixelsToVariable(*bitmap, frame_rect, dst_var_id, ignore_alpha, *Main_Data::game_variables)) {
return true;
}

Game_Map::SetNeedRefresh(true);
return true;
}

// Logic for Info Types 0, 1, 2
int x = 0;
int y = 0;
int width = pic.sprite ? pic.sprite->GetWidth() : 0;
int height = pic.sprite ? pic.sprite->GetHeight() : 0;
int width = 0;
int height = 0;

switch (com.parameters[1]) {
switch (info_type) {
case 0:
x = Utils::RoundTo<int>(data.current_x);
y = Utils::RoundTo<int>(data.current_y);
width = pic.sprite ? pic.sprite->GetWidth() : 0;
height = pic.sprite ? pic.sprite->GetHeight() : 0;
break;
case 1:
x = Utils::RoundTo<int>(data.current_x);
Expand All @@ -4741,6 +4806,9 @@ bool Game_Interpreter::CommandManiacGetPictureInfo(lcf::rpg::EventCommand const&
}

switch (com.parameters[2]) {
case 0:
// X/Y is center
break;
case 1:
// X/Y is top-left corner
x -= (width / 2);
Expand Down Expand Up @@ -5314,6 +5382,75 @@ bool Game_Interpreter::CommandManiacControlStrings(lcf::rpg::EventCommand const&
return true;
}

bool Game_Interpreter::CommandManiacEditPicture(lcf::rpg::EventCommand const& com) {
if (!Player::IsPatchManiac()) {
return true;
}

int pic_id = ValueOrVariableBitfield(com.parameters[0], 0, com.parameters[1]);
if (pic_id <= 0) {
Output::Warning("ManiacSetPicturePixel: Invalid picture ID {}", pic_id);
return true;
}

auto& picture = Main_Data::game_pictures->GetPicture(pic_id);

if (picture.IsRequestPending()) {
picture.MakeRequestImportant();
_async_op = AsyncOp::MakeYieldRepeat();
return true;
}

auto* sprite = picture.sprite.get();
auto bitmap = sprite->GetBitmap();

// Calculate Spritesheet Offset
// Maniacs operations are relative to the currently active cell.

// Determine Spritesheet frame
Rect src_rect = sprite->GetSrcRect();

BitmapRef writable_bitmap = bitmap;

if (picture.IsWindowAttached()) {
// If this is a Window (String Picture), the visual content is generated by the Window class.
// We force a refresh/draw cycle here to ensure we read the actual text/window graphics.
const auto& window = Main_Data::game_windows->GetWindow(pic_id);
if (window.window) {
bitmap->Clear();
window.window->Draw(*bitmap);
}
} else if (picture.IsCanvas()) {
// no-op
} else {
// Must be copied to avoid modifiying the original picture
writable_bitmap = Bitmap::Create(*bitmap, src_rect, true);
}

picture.data.name = {};
picture.data.easyrpg_type = lcf::rpg::SavePicture::EasyRpgType_canvas;

// Packing: x pos, y pos, width, height, var_id
int pic_x = ValueOrVariableBitfield(com.parameters[0], 1, com.parameters[2]);
int pic_y = ValueOrVariableBitfield(com.parameters[0], 2, com.parameters[3]);
int pic_w = ValueOrVariableBitfield(com.parameters[0], 3, com.parameters[4]);
int pic_h = ValueOrVariableBitfield(com.parameters[0], 4, com.parameters[5]);
int start_var_id = ValueOrVariableBitfield(com.parameters[0], 5, com.parameters[6]);

int flags = com.parameters[7];
// When no flag is set the area is cleared and a OP_OVER blit occurs
bool flag_opaq = (flags & 1) != 0; // Blit with OP_SRC
bool flag_skip_trans = (flags & 2) != 0; // Blit with OP_OVER

bool clear_dst = !flag_opaq && !flag_skip_trans;
bool ignore_alpha = flag_opaq;

Rect frame_rect{pic_x, pic_y, pic_w, pic_h};
ManiacPatch::ReadPixelsFromVariable(*writable_bitmap, frame_rect, start_var_id, clear_dst, ignore_alpha, *Main_Data::game_variables);

return true;
}

bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& com) {
if (!Player::IsPatchManiac()) {
return true;
Expand Down Expand Up @@ -5376,8 +5513,17 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c
const auto sprite = picture.sprite.get();

// Retrieve bitmap
// Cannot change transparency of images that are not reloadable from a file (window and canvas)
// Appears to match Maniacs behaviour
if (picture.IsWindowAttached()) {
// Maniac ignores the opaque setting for String Picture
// If this is a Window (String Picture), the visual content is generated by the Window class.
// We force a refresh/draw cycle here to ensure we read the actual text/window graphics.
const auto& window = Main_Data::game_windows->GetWindow(pic_id);
if (window.window) {
bitmap->Clear();
window.window->Draw(*bitmap);
}
} else if (picture.IsCanvas()) {
bitmap = picture.sprite->GetBitmap();
} else if (picture.data.name.empty()) {
// Not much we can do here (also shouldn't happen normally)
Expand Down Expand Up @@ -5417,11 +5563,11 @@ bool Game_Interpreter::CommandManiacWritePicture(lcf::rpg::EventCommand const& c
filename += ".png";
}

auto found_file = FileFinder::Save().FindFile(filename);

auto os = FileFinder::Save().OpenOutputStream(found_file.empty() ? filename : found_file);
if (os) {
bitmap->WritePNG(os);
auto img_out = FileFinder::OpenWrite(filename);
if (img_out) {
bitmap->WritePNG(img_out);
// Not ideal but figuring out the exact cache entry is complicated
Cache::Invalidate("Picture");
} else {
Output::Warning("ManiacSaveImage: Failed to open file for writing: {}", filename);
}
Expand Down
3 changes: 2 additions & 1 deletion src/game_interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,14 @@ class Game_Interpreter : public Game_BaseInterpreterContext
bool CommandManiacChangePictureId(lcf::rpg::EventCommand const& com);
bool CommandManiacSetGameOption(lcf::rpg::EventCommand const& com);
bool CommandManiacControlStrings(lcf::rpg::EventCommand const& com);
bool CommandManiacEditPicture(lcf::rpg::EventCommand const& com);
bool CommandManiacWritePicture(lcf::rpg::EventCommand const& com);
bool CommandManiacCallCommand(lcf::rpg::EventCommand const& com);
bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgProcessJson(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgCloneMapEvent(lcf::rpg::EventCommand const& com);
bool CommandEasyRpgDestroyMapEvent(lcf::rpg::EventCommand const& com);
bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com);

void SetSubcommandIndex(int indent, int idx);
uint8_t& ReserveSubcommandIndex(int indent);
Expand Down
Loading
Loading