scummvm/engines/ags/engine/ac/draw.cpp

2563 lines
90 KiB
C++

//=============================================================================
//
// Adventure Game Studio (AGS)
//
// Copyright (C) 1999-2011 Chris Jones and 2011-20xx others
// The full list of copyright holders can be found in the Copyright.txt
// file, which is part of this source code distribution.
//
// The AGS source code is provided under the Artistic License 2.0.
// A copy of this license can be found in the file License.txt and at
// http://www.opensource.org/licenses/artistic-license-2.0.php
//
//=============================================================================
#include <algorithm>
#include <cmath>
#include "aastr.h"
#include "core/platform.h"
#include "ac/common.h"
#include "util/compress.h"
#include "ac/view.h"
#include "ac/charactercache.h"
#include "ac/characterextras.h"
#include "ac/characterinfo.h"
#include "ac/display.h"
#include "ac/draw.h"
#include "ac/draw_software.h"
#include "ac/gamesetup.h"
#include "ac/gamesetupstruct.h"
#include "ac/gamestate.h"
#include "ac/global_game.h"
#include "ac/global_gui.h"
#include "ac/global_region.h"
#include "ac/gui.h"
#include "ac/mouse.h"
#include "ac/objectcache.h"
#include "ac/overlay.h"
#include "ac/sys_events.h"
#include "ac/roomobject.h"
#include "ac/roomstatus.h"
#include "ac/runtime_defines.h"
#include "ac/screenoverlay.h"
#include "ac/sprite.h"
#include "ac/spritelistentry.h"
#include "ac/string.h"
#include "ac/system.h"
#include "ac/viewframe.h"
#include "ac/walkablearea.h"
#include "ac/walkbehind.h"
#include "ac/dynobj/scriptsystem.h"
#include "debug/debugger.h"
#include "debug/debug_log.h"
#include "font/fonts.h"
#include "gui/guimain.h"
#include "platform/base/agsplatformdriver.h"
#include "plugin/agsplugin.h"
#include "plugin/plugin_engine.h"
#include "ac/spritecache.h"
#include "gfx/gfx_util.h"
#include "gfx/graphicsdriver.h"
#include "gfx/ali3dexception.h"
#include "gfx/blender.h"
#include "media/audio/audio_system.h"
#include "ac/game.h"
using namespace AGS::Common;
using namespace AGS::Engine;
#if AGS_PLATFORM_OS_ANDROID
#include <sys/stat.h>
#include <android/log.h>
extern "C" void android_render();
#endif
#if AGS_PLATFORM_OS_IOS
extern "C" void ios_render();
#endif
extern GameSetup usetup;
extern GameSetupStruct game;
extern GameState play;
extern int convert_16bit_bgr;
extern ScriptSystem scsystem;
extern AGSPlatformDriver *platform;
extern RoomStruct thisroom;
extern char noWalkBehindsAtAll;
extern unsigned int loopcounter;
extern char *walkBehindExists; // whether a WB area is in this column
extern int *walkBehindStartY, *walkBehindEndY;
extern int walkBehindLeft[MAX_WALK_BEHINDS], walkBehindTop[MAX_WALK_BEHINDS];
extern int walkBehindRight[MAX_WALK_BEHINDS], walkBehindBottom[MAX_WALK_BEHINDS];
extern IDriverDependantBitmap *walkBehindBitmap[MAX_WALK_BEHINDS];
extern int walkBehindsCachedForBgNum;
extern WalkBehindMethodEnum walkBehindMethod;
extern int walk_behind_baselines_changed;
extern SpriteCache spriteset;
extern RoomStatus*croom;
extern int our_eip;
extern int in_new_room;
extern RoomObject*objs;
extern ViewStruct*views;
extern CharacterCache *charcache;
extern ObjectCache objcache[MAX_ROOM_OBJECTS];
extern int displayed_room;
extern CharacterExtras *charextra;
extern CharacterInfo*playerchar;
extern int eip_guinum;
extern int is_complete_overlay;
extern int cur_mode,cur_cursor;
extern int mouse_frame,mouse_delay;
extern int lastmx,lastmy;
extern IDriverDependantBitmap *mouseCursor;
extern int hotx,hoty;
extern int bg_just_changed;
color palette[256];
COLOR_MAP maincoltable;
IGraphicsDriver *gfxDriver;
IDriverDependantBitmap *blankImage = nullptr;
IDriverDependantBitmap *blankSidebarImage = nullptr;
IDriverDependantBitmap *debugConsole = nullptr;
// actsps is used for temporary storage of the bitamp image
// of the latest version of the sprite
int actSpsCount = 0;
Bitmap **actsps;
IDriverDependantBitmap* *actspsbmp;
// temporary cache of walk-behind for this actsps image
Bitmap **actspswb;
IDriverDependantBitmap* *actspswbbmp;
CachedActSpsData* actspswbcache;
bool current_background_is_dirty = false;
// Room background sprite
IDriverDependantBitmap* roomBackgroundBmp = nullptr;
// Buffer and info flags for viewport/camera pairs rendering in software mode
struct RoomCameraDrawData
{
// Intermediate bitmap for the software drawing method.
// We use this bitmap in case room camera has scaling enabled, we draw dirty room rects on it,
// and then pass to software renderer which draws sprite on top and then either blits or stretch-blits
// to the virtual screen.
// For more details see comment in ALSoftwareGraphicsDriver::RenderToBackBuffer().
PBitmap Buffer; // this is the actual bitmap
PBitmap Frame; // this is either same bitmap reference or sub-bitmap of virtual screen
bool IsOffscreen; // whether room viewport was offscreen (cannot use sub-bitmap)
bool IsOverlap; // whether room viewport overlaps any others (marking dirty rects is complicated)
};
std::vector<RoomCameraDrawData> CameraDrawData;
std::vector<SpriteListEntry> sprlist;
std::vector<SpriteListEntry> thingsToDrawList;
Bitmap **guibg = nullptr;
IDriverDependantBitmap **guibgbmp = nullptr;
Bitmap *debugConsoleBuffer = nullptr;
// whether there are currently remnants of a DisplaySpeech
bool screen_is_dirty = false;
Bitmap *raw_saved_screen = nullptr;
Bitmap *dynamicallyCreatedSurfaces[MAX_DYNAMIC_SURFACES];
SpriteListEntry::SpriteListEntry()
: bmp(nullptr)
, pic(nullptr)
, baseline(0), x(0), y(0)
, transparent(0)
, takesPriorityIfEqual(false), hasAlphaChannel(false)
{
}
void setpal() {
set_palette_range(palette, 0, 255, 0);
}
int _places_r = 3, _places_g = 2, _places_b = 3;
// convert RGB to BGR for strange graphics cards
Bitmap *convert_16_to_16bgr(Bitmap *tempbl) {
int x,y;
unsigned short c,r,ds,b;
for (y=0; y < tempbl->GetHeight(); y++) {
unsigned short*p16 = (unsigned short *)tempbl->GetScanLine(y);
for (x=0; x < tempbl->GetWidth(); x++) {
c = p16[x];
if (c != MASK_COLOR_16) {
b = _rgb_scale_5[c & 0x1F];
ds = _rgb_scale_6[(c >> 5) & 0x3F];
r = _rgb_scale_5[(c >> 11) & 0x1F];
// allegro assumes 5-6-5 for 16-bit
p16[x] = (((r >> _places_r) << _rgb_r_shift_16) |
((ds >> _places_g) << _rgb_g_shift_16) |
((b >> _places_b) << _rgb_b_shift_16));
}
}
}
return tempbl;
}
// PSP: convert 32 bit RGB to BGR.
Bitmap *convert_32_to_32bgr(Bitmap *tempbl) {
int i = 0;
int j = 0;
unsigned char* current;
while (i < tempbl->GetHeight())
{
current = tempbl->GetScanLineForWriting(i);
while (j < tempbl->GetWidth())
{
current[0] ^= current[2];
current[2] ^= current[0];
current[0] ^= current[2];
current += 4;
j++;
}
i++;
j = 0;
}
return tempbl;
}
// NOTE: Some of these conversions are required even when using
// D3D and OpenGL rendering, for two reasons:
// 1) certain raw drawing operations are still performed by software
// Allegro methods, hence bitmaps should be kept compatible to any native
// software operations, such as blitting two bitmaps of different formats.
// 2) mobile ports feature an OpenGL renderer built in Allegro library,
// that assumes native bitmaps are in OpenGL-compatible format, so that it
// could copy them to texture without additional changes.
// AGS own OpenGL renderer tries to sync its behavior with the former one.
//
// TODO: make gfxDriver->GetCompatibleBitmapFormat describe all necessary
// conversions, so that we did not have to guess.
//
Bitmap *AdjustBitmapForUseWithDisplayMode(Bitmap* bitmap, bool has_alpha)
{
const int bmp_col_depth = bitmap->GetColorDepth();
const int sys_col_depth = System_GetColorDepth();
const int game_col_depth = game.GetColorDepth();
Bitmap *new_bitmap = bitmap;
//
// The only special case when bitmap needs to be prepared for graphics driver
//
// In 32-bit display mode, 32-bit bitmaps may require component conversion
// to match graphics driver expectation about pixel format.
// TODO: make GetCompatibleBitmapFormat tell this somehow
#if defined (AGS_INVERTED_COLOR_ORDER)
if (sys_col_depth > 16 && bmp_col_depth == 32)
{
// Convert RGB to BGR.
new_bitmap = convert_32_to_32bgr(bitmap);
}
#endif
//
// The rest is about bringing bitmaps to the native game's format
// (has no dependency on display mode).
//
// In 32-bit game 32-bit bitmaps should have transparent pixels marked
// (this adjustment is probably needed for DrawingSurface ops)
if (game_col_depth == 32 && bmp_col_depth == 32)
{
if (has_alpha)
set_rgb_mask_using_alpha_channel(new_bitmap);
}
// In 32-bit game hicolor bitmaps must be converted to the true color
else if (game_col_depth == 32 && (bmp_col_depth > 8 && bmp_col_depth <= 16))
{
new_bitmap = BitmapHelper::CreateBitmapCopy(bitmap, game_col_depth);
}
// In non-32-bit game truecolor bitmaps must be downgraded
else if (game_col_depth <= 16 && bmp_col_depth > 16)
{
if (has_alpha) // if has valid alpha channel, convert it to regular transparency mask
new_bitmap = remove_alpha_channel(bitmap);
else // else simply convert bitmap
new_bitmap = BitmapHelper::CreateBitmapCopy(bitmap, game_col_depth);
}
// Special case when we must convert 16-bit RGB to BGR
else if (convert_16bit_bgr == 1 && bmp_col_depth == 16)
{
new_bitmap = convert_16_to_16bgr(bitmap);
}
return new_bitmap;
}
Bitmap *ReplaceBitmapWithSupportedFormat(Bitmap *bitmap)
{
Bitmap *new_bitmap = GfxUtil::ConvertBitmap(bitmap, gfxDriver->GetCompatibleBitmapFormat(bitmap->GetColorDepth()));
if (new_bitmap != bitmap)
delete bitmap;
return new_bitmap;
}
Bitmap *PrepareSpriteForUse(Bitmap* bitmap, bool has_alpha)
{
bool must_switch_palette = bitmap->GetColorDepth() == 8 && game.GetColorDepth() > 8;
if (must_switch_palette)
select_palette(palette);
Bitmap *new_bitmap = AdjustBitmapForUseWithDisplayMode(bitmap, has_alpha);
if (new_bitmap != bitmap)
delete bitmap;
new_bitmap = ReplaceBitmapWithSupportedFormat(new_bitmap);
if (must_switch_palette)
unselect_palette();
return new_bitmap;
}
PBitmap PrepareSpriteForUse(PBitmap bitmap, bool has_alpha)
{
bool must_switch_palette = bitmap->GetColorDepth() == 8 && System_GetColorDepth() > 8;
if (must_switch_palette)
select_palette(palette);
Bitmap *new_bitmap = AdjustBitmapForUseWithDisplayMode(bitmap.get(), has_alpha);
new_bitmap = ReplaceBitmapWithSupportedFormat(new_bitmap);
if (must_switch_palette)
unselect_palette();
return new_bitmap == bitmap.get() ? bitmap : PBitmap(new_bitmap); // if bitmap is same, don't create new smart ptr!
}
Bitmap *CopyScreenIntoBitmap(int width, int height, bool at_native_res)
{
Bitmap *dst = new Bitmap(width, height, game.GetColorDepth());
GraphicResolution want_fmt;
// If the size and color depth are supported we may copy right into our bitmap
if (gfxDriver->GetCopyOfScreenIntoBitmap(dst, at_native_res, &want_fmt))
return dst;
// Otherwise we might need to copy between few bitmaps...
Bitmap *buf_screenfmt = new Bitmap(want_fmt.Width, want_fmt.Height, want_fmt.ColorDepth);
gfxDriver->GetCopyOfScreenIntoBitmap(buf_screenfmt, at_native_res);
// If at least size matches then we may blit
if (dst->GetSize() == buf_screenfmt->GetSize())
{
dst->Blit(buf_screenfmt);
}
// Otherwise we need to go through another bitmap of the matching format
else
{
Bitmap *buf_dstfmt = new Bitmap(buf_screenfmt->GetWidth(), buf_screenfmt->GetHeight(), dst->GetColorDepth());
buf_dstfmt->Blit(buf_screenfmt);
dst->StretchBlt(buf_dstfmt, RectWH(dst->GetSize()));
delete buf_dstfmt;
}
delete buf_screenfmt;
return dst;
}
// Begin resolution system functions
// Multiplies up the number of pixels depending on the current
// resolution, to give a relatively fixed size at any game res
AGS_INLINE int get_fixed_pixel_size(int pixels)
{
return pixels * game.GetRelativeUIMult();
}
AGS_INLINE int data_to_game_coord(int coord)
{
return coord * game.GetDataUpscaleMult();
}
AGS_INLINE void data_to_game_coords(int *x, int *y)
{
const int mul = game.GetDataUpscaleMult();
x[0] *= mul;
y[0] *= mul;
}
AGS_INLINE void data_to_game_round_up(int *x, int *y)
{
const int mul = game.GetDataUpscaleMult();
x[0] = x[0] * mul + (mul - 1);
y[0] = y[0] * mul + (mul - 1);
}
AGS_INLINE int game_to_data_coord(int coord)
{
return coord / game.GetDataUpscaleMult();
}
AGS_INLINE void game_to_data_coords(int &x, int &y)
{
const int mul = game.GetDataUpscaleMult();
x /= mul;
y /= mul;
}
AGS_INLINE int game_to_data_round_up(int coord)
{
const int mul = game.GetDataUpscaleMult();
return (coord / mul) + (mul - 1);
}
AGS_INLINE void ctx_data_to_game_coord(int &x, int &y, bool hires_ctx)
{
if (hires_ctx && !game.IsLegacyHiRes())
{
x /= HIRES_COORD_MULTIPLIER;
y /= HIRES_COORD_MULTIPLIER;
}
else if (!hires_ctx && game.IsLegacyHiRes())
{
x *= HIRES_COORD_MULTIPLIER;
y *= HIRES_COORD_MULTIPLIER;
}
}
AGS_INLINE void ctx_data_to_game_size(int &w, int &h, bool hires_ctx)
{
if (hires_ctx && !game.IsLegacyHiRes())
{
w = Math::Max(1, (w / HIRES_COORD_MULTIPLIER));
h = Math::Max(1, (h / HIRES_COORD_MULTIPLIER));
}
else if (!hires_ctx && game.IsLegacyHiRes())
{
w *= HIRES_COORD_MULTIPLIER;
h *= HIRES_COORD_MULTIPLIER;
}
}
AGS_INLINE int ctx_data_to_game_size(int size, bool hires_ctx)
{
if (hires_ctx && !game.IsLegacyHiRes())
return Math::Max(1, (size / HIRES_COORD_MULTIPLIER));
if (!hires_ctx && game.IsLegacyHiRes())
return size * HIRES_COORD_MULTIPLIER;
return size;
}
AGS_INLINE int game_to_ctx_data_size(int size, bool hires_ctx)
{
if (hires_ctx && !game.IsLegacyHiRes())
return size * HIRES_COORD_MULTIPLIER;
else if (!hires_ctx && game.IsLegacyHiRes())
return Math::Max(1, (size / HIRES_COORD_MULTIPLIER));
return size;
}
AGS_INLINE void defgame_to_finalgame_coords(int &x, int &y)
{ // Note we support only upscale now
x *= game.GetScreenUpscaleMult();
y *= game.GetScreenUpscaleMult();
}
// End resolution system functions
// Create blank (black) images used to repaint borders around game frame
void create_blank_image(int coldepth)
{
// this is the first time that we try to use the graphics driver,
// so it's the most likey place for a crash
try
{
Bitmap *blank = BitmapHelper::CreateBitmap(16, 16, coldepth);
blank = ReplaceBitmapWithSupportedFormat(blank);
blank->Clear();
blankImage = gfxDriver->CreateDDBFromBitmap(blank, false, true);
blankSidebarImage = gfxDriver->CreateDDBFromBitmap(blank, false, true);
delete blank;
}
catch (Ali3DException gfxException)
{
quit((char*)gfxException._message);
}
}
void destroy_blank_image()
{
if (blankImage)
gfxDriver->DestroyDDB(blankImage);
if (blankSidebarImage)
gfxDriver->DestroyDDB(blankSidebarImage);
blankImage = nullptr;
blankSidebarImage = nullptr;
}
int MakeColor(int color_index)
{
color_t real_color = 0;
__my_setcolor(&real_color, color_index, game.GetColorDepth());
return real_color;
}
void init_draw_method()
{
if (gfxDriver->HasAcceleratedTransform())
{
walkBehindMethod = DrawAsSeparateSprite;
create_blank_image(game.GetColorDepth());
}
else
{
walkBehindMethod = DrawOverCharSprite;
}
on_mainviewport_changed();
init_room_drawdata();
if (gfxDriver->UsesMemoryBackBuffer())
gfxDriver->GetMemoryBackBuffer()->Clear();
}
void dispose_draw_method()
{
dispose_room_drawdata();
dispose_invalid_regions(false);
destroy_blank_image();
}
void dispose_room_drawdata()
{
CameraDrawData.clear();
dispose_invalid_regions(true);
}
void on_mainviewport_changed()
{
if (!gfxDriver->RequiresFullRedrawEachFrame())
{
init_invalid_regions(-1, play.GetMainViewport().GetSize(), RectWH(play.GetMainViewport().GetSize()));
if (game.GetGameRes().ExceedsByAny(play.GetMainViewport().GetSize()))
clear_letterbox_borders();
}
}
// Allocates a bitmap for rendering camera/viewport pair (software render mode)
void prepare_roomview_frame(Viewport *view)
{
const int view_index = view->GetID();
const Size view_sz = view->GetRect().GetSize();
const Size cam_sz = view->GetCamera()->GetRect().GetSize();
RoomCameraDrawData &draw_dat = CameraDrawData[view_index];
// We use intermediate bitmap to render camera/viewport pair in software mode under these conditions:
// * camera size and viewport size are different (this may be suboptimal to paint dirty rects stretched,
// and also Allegro backend cannot stretch background of different colour depth).
// * viewport is located outside of the virtual screen (even if partially): subbitmaps cannot contain
// regions outside of master bitmap, and we must not clamp surface size to virtual screen because
// plugins may want to also use viewport bitmap, therefore it should retain full size.
if (cam_sz == view_sz && !draw_dat.IsOffscreen)
{ // note we keep the buffer allocated in case it will become useful later
draw_dat.Frame.reset();
}
else
{
PBitmap &camera_frame = draw_dat.Frame;
PBitmap &camera_buffer = draw_dat.Buffer;
if (!camera_buffer || camera_buffer->GetWidth() < cam_sz.Width || camera_buffer->GetHeight() < cam_sz.Height)
{
// Allocate new buffer bitmap with an extra size in case they will want to zoom out
int room_width = data_to_game_coord(thisroom.Width);
int room_height = data_to_game_coord(thisroom.Height);
Size alloc_sz = Size::Clamp(cam_sz * 2, Size(1, 1), Size(room_width, room_height));
camera_buffer.reset(new Bitmap(alloc_sz.Width, alloc_sz.Height, gfxDriver->GetMemoryBackBuffer()->GetColorDepth()));
}
if (!camera_frame || camera_frame->GetSize() != cam_sz)
{
camera_frame.reset(BitmapHelper::CreateSubBitmap(camera_buffer.get(), RectWH(cam_sz)));
}
}
}
// Syncs room viewport and camera in case either size has changed
void sync_roomview(Viewport *view)
{
if (view->GetCamera() == nullptr)
return;
init_invalid_regions(view->GetID(), view->GetCamera()->GetRect().GetSize(), play.GetRoomViewportAbs(view->GetID()));
prepare_roomview_frame(view);
}
void init_room_drawdata()
{
if (gfxDriver->RequiresFullRedrawEachFrame())
return;
// Make sure all frame buffers are created for software drawing
int view_count = play.GetRoomViewportCount();
CameraDrawData.resize(view_count);
for (int i = 0; i < play.GetRoomViewportCount(); ++i)
sync_roomview(play.GetRoomViewport(i).get());
}
void on_roomviewport_created(int index)
{
if (!gfxDriver || gfxDriver->RequiresFullRedrawEachFrame())
return;
if ((size_t)index < CameraDrawData.size())
return;
CameraDrawData.resize(index + 1);
}
void on_roomviewport_deleted(int index)
{
if (gfxDriver->RequiresFullRedrawEachFrame())
return;
CameraDrawData.erase(CameraDrawData.begin() + index);
delete_invalid_regions(index);
}
void on_roomviewport_changed(Viewport *view)
{
if (gfxDriver->RequiresFullRedrawEachFrame())
return;
if (!view->IsVisible() || view->GetCamera() == nullptr)
return;
const bool off = !IsRectInsideRect(RectWH(gfxDriver->GetMemoryBackBuffer()->GetSize()), view->GetRect());
const bool off_changed = off != CameraDrawData[view->GetID()].IsOffscreen;
CameraDrawData[view->GetID()].IsOffscreen = off;
if (view->HasChangedSize())
sync_roomview(view);
else if (off_changed)
prepare_roomview_frame(view);
// TODO: don't have to do this all the time, perhaps do "dirty rect" method
// and only clear previous viewport location?
invalidate_screen();
gfxDriver->GetMemoryBackBuffer()->Clear();
}
void detect_roomviewport_overlaps(size_t z_index)
{
if (gfxDriver->RequiresFullRedrawEachFrame())
return;
// Find out if we overlap or are overlapped by anything;
const auto &viewports = play.GetRoomViewportsZOrdered();
for (; z_index < viewports.size(); ++z_index)
{
auto this_view = viewports[z_index];
const int this_id = this_view->GetID();
bool is_overlap = false;
if (!this_view->IsVisible()) continue;
for (size_t z_index2 = 0; z_index2 < z_index; ++z_index)
{
if (!viewports[z_index2]->IsVisible()) continue;
if (AreRectsIntersecting(this_view->GetRect(), viewports[z_index2]->GetRect()))
{
is_overlap = true;
break;
}
}
if (CameraDrawData[this_id].IsOverlap != is_overlap)
{
CameraDrawData[this_id].IsOverlap = is_overlap;
prepare_roomview_frame(this_view.get());
}
}
}
void on_roomcamera_changed(Camera *cam)
{
if (gfxDriver->RequiresFullRedrawEachFrame())
return;
if (cam->HasChangedSize())
{
auto viewrefs = cam->GetLinkedViewports();
for (auto vr : viewrefs)
{
PViewport vp = vr.lock();
if (vp)
sync_roomview(vp.get());
}
}
// TODO: only invalidate what this particular camera sees
invalidate_screen();
}
void mark_screen_dirty()
{
screen_is_dirty = true;
}
bool is_screen_dirty()
{
return screen_is_dirty;
}
void invalidate_screen()
{
invalidate_all_rects();
}
void invalidate_camera_frame(int index)
{
invalidate_all_camera_rects(index);
}
void invalidate_rect(int x1, int y1, int x2, int y2, bool in_room)
{
//if (!in_room)
invalidate_rect_ds(x1, y1, x2, y2, in_room);
}
void invalidate_sprite(int x1, int y1, IDriverDependantBitmap *pic, bool in_room)
{
//if (!in_room)
invalidate_rect_ds(x1, y1, x1 + pic->GetWidth(), y1 + pic->GetHeight(), in_room);
}
void mark_current_background_dirty()
{
current_background_is_dirty = true;
}
void draw_and_invalidate_text(Bitmap *ds, int x1, int y1, int font, color_t text_color, const char *text)
{
wouttext_outline(ds, x1, y1, font, text_color, (char*)text);
invalidate_rect(x1, y1, x1 + wgettextwidth_compensate(text, font), y1 + getfontheight_outlined(font) + get_fixed_pixel_size(1), false);
}
// Renders black borders for the legacy boxed game mode,
// where whole game screen changes size between large and small rooms
void render_black_borders()
{
if (gfxDriver->UsesMemoryBackBuffer())
return;
{
gfxDriver->BeginSpriteBatch(RectWH(game.GetGameRes()), SpriteTransform());
const Rect &viewport = play.GetMainViewport();
if (viewport.Top > 0)
{
// letterbox borders
blankImage->SetStretch(game.GetGameRes().Width, viewport.Top, false);
gfxDriver->DrawSprite(0, 0, blankImage);
gfxDriver->DrawSprite(0, viewport.Bottom + 1, blankImage);
}
if (viewport.Left > 0)
{
// sidebar borders for widescreen
blankSidebarImage->SetStretch(viewport.Left, viewport.GetHeight(), false);
gfxDriver->DrawSprite(0, 0, blankSidebarImage);
gfxDriver->DrawSprite(viewport.Right + 1, 0, blankSidebarImage);
}
}
}
void render_to_screen()
{
// Stage: final plugin callback (still drawn on game screen
if (pl_any_want_hook(AGSE_FINALSCREENDRAW))
{
gfxDriver->BeginSpriteBatch(play.GetMainViewport(), SpriteTransform(), Point(0, play.shake_screen_yoff), (GlobalFlipType)play.screen_flipped);
gfxDriver->DrawSprite(AGSE_FINALSCREENDRAW, 0, nullptr);
}
// Stage: engine overlay
construct_engine_overlay();
// only vsync in full screen mode, it makes things worse in a window
gfxDriver->EnableVsyncBeforeRender((scsystem.vsync > 0) && (!scsystem.windowed));
bool succeeded = false;
while (!succeeded)
{
try
{
// For software renderer, need to blacken upper part of the game frame when shaking screen moves image down
const Rect &viewport = play.GetMainViewport();
if (play.shake_screen_yoff > 0 && !gfxDriver->RequiresFullRedrawEachFrame())
gfxDriver->ClearRectangle(viewport.Left, viewport.Top, viewport.GetWidth() - 1, play.shake_screen_yoff, nullptr);
gfxDriver->Render(0, play.shake_screen_yoff, (GlobalFlipType)play.screen_flipped);
#if AGS_PLATFORM_OS_ANDROID
if (game.color_depth == 1)
android_render();
#elif AGS_PLATFORM_OS_IOS
if (game.color_depth == 1)
ios_render();
#endif
succeeded = true;
}
catch (Ali3DFullscreenLostException)
{
platform->Delay(500);
}
}
}
// Blanks out borders around main viewport in case it became smaller (e.g. after loading another room)
void clear_letterbox_borders()
{
const Rect &viewport = play.GetMainViewport();
gfxDriver->ClearRectangle(0, 0, game.GetGameRes().Width - 1, viewport.Top - 1, nullptr);
gfxDriver->ClearRectangle(0, viewport.Bottom + 1, game.GetGameRes().Width - 1, game.GetGameRes().Height - 1, nullptr);
}
void draw_game_screen_callback()
{
construct_game_scene(true);
construct_game_screen_overlay(false);
}
void putpixel_compensate (Bitmap *ds, int xx,int yy, int col) {
if ((ds->GetColorDepth() == 32) && (col != 0)) {
// ensure the alpha channel is preserved if it has one
int alphaval = geta32(ds->GetPixel(xx, yy));
col = makeacol32(getr32(col), getg32(col), getb32(col), alphaval);
}
ds->FillRect(Rect(xx, yy, xx + get_fixed_pixel_size(1) - 1, yy + get_fixed_pixel_size(1) - 1), col);
}
void draw_sprite_support_alpha(Bitmap *ds, bool ds_has_alpha, int xpos, int ypos, Bitmap *image, bool src_has_alpha,
BlendMode blend_mode, int alpha)
{
if (alpha <= 0)
return;
if (game.options[OPT_SPRITEALPHA] == kSpriteAlphaRender_Proper)
{
GfxUtil::DrawSpriteBlend(ds, Point(xpos, ypos), image, blend_mode, ds_has_alpha, src_has_alpha, alpha);
}
// Backwards-compatible drawing
else if (src_has_alpha && alpha == 0xFF)
{
set_alpha_blender();
ds->TransBlendBlt(image, xpos, ypos);
}
else
{
GfxUtil::DrawSpriteWithTransparency(ds, image, xpos, ypos, alpha);
}
}
void draw_sprite_slot_support_alpha(Bitmap *ds, bool ds_has_alpha, int xpos, int ypos, int src_slot,
BlendMode blend_mode, int alpha)
{
draw_sprite_support_alpha(ds, ds_has_alpha, xpos, ypos, spriteset[src_slot], (game.SpriteInfos[src_slot].Flags & SPF_ALPHACHANNEL) != 0,
blend_mode, alpha);
}
IDriverDependantBitmap* recycle_ddb_bitmap(IDriverDependantBitmap *bimp, Bitmap *source, bool hasAlpha, bool opaque) {
if (bimp != nullptr) {
// same colour depth, width and height -> reuse
if (((bimp->GetColorDepth() + 1) / 8 == source->GetBPP()) &&
(bimp->GetWidth() == source->GetWidth()) && (bimp->GetHeight() == source->GetHeight()))
{
gfxDriver->UpdateDDBFromBitmap(bimp, source, hasAlpha);
return bimp;
}
gfxDriver->DestroyDDB(bimp);
}
bimp = gfxDriver->CreateDDBFromBitmap(source, hasAlpha, opaque);
return bimp;
}
void invalidate_cached_walkbehinds()
{
memset(&actspswbcache[0], 0, sizeof(CachedActSpsData) * actSpsCount);
}
// sort_out_walk_behinds: modifies the supplied sprite by overwriting parts
// of it with transparent pixels where there are walk-behind areas
// Returns whether any pixels were updated
int sort_out_walk_behinds(Bitmap *sprit,int xx,int yy,int basel, Bitmap *copyPixelsFrom = nullptr, Bitmap *checkPixelsFrom = nullptr, int zoom=100) {
if (noWalkBehindsAtAll)
return 0;
if ((!thisroom.WalkBehindMask->IsMemoryBitmap()) ||
(!sprit->IsMemoryBitmap()))
quit("!sort_out_walk_behinds: wb bitmap not linear");
int rr,tmm, toheight;//,tcol;
// precalculate this to try and shave some time off
int maskcol = sprit->GetMaskColor();
int spcoldep = sprit->GetColorDepth();
int screenhit = thisroom.WalkBehindMask->GetHeight();
short *shptr, *shptr2;
int *loptr, *loptr2;
int pixelsChanged = 0;
int ee = 0;
if (xx < 0)
ee = 0 - xx;
if ((checkPixelsFrom != nullptr) && (checkPixelsFrom->GetColorDepth() != spcoldep))
quit("sprite colour depth does not match background colour depth");
for ( ; ee < sprit->GetWidth(); ee++) {
if (ee + xx >= thisroom.WalkBehindMask->GetWidth())
break;
if ((!walkBehindExists[ee+xx]) ||
(walkBehindEndY[ee+xx] <= yy) ||
(walkBehindStartY[ee+xx] > yy+sprit->GetHeight()))
continue;
toheight = sprit->GetHeight();
if (walkBehindStartY[ee+xx] < yy)
rr = 0;
else
rr = (walkBehindStartY[ee+xx] - yy);
// Since we will use _getpixel, ensure we only check within the screen
if (rr + yy < 0)
rr = 0 - yy;
if (toheight + yy > screenhit)
toheight = screenhit - yy;
if (toheight + yy > walkBehindEndY[ee+xx])
toheight = walkBehindEndY[ee+xx] - yy;
if (rr < 0)
rr = 0;
for ( ; rr < toheight;rr++) {
// we're ok with _getpixel because we've checked the screen edges
//tmm = _getpixel(thisroom.WalkBehindMask,ee+xx,rr+yy);
// actually, _getpixel is well inefficient, do it ourselves
// since we know it's 8-bit bitmap
tmm = thisroom.WalkBehindMask->GetScanLine(rr+yy)[ee+xx];
if (tmm<1) continue;
if (croom->walkbehind_base[tmm] <= basel) continue;
if (copyPixelsFrom != nullptr)
{
if (spcoldep <= 8)
{
if (checkPixelsFrom->GetScanLine((rr * 100) / zoom)[(ee * 100) / zoom] != maskcol) {
sprit->GetScanLineForWriting(rr)[ee] = copyPixelsFrom->GetScanLine(rr + yy)[ee + xx];
pixelsChanged = 1;
}
}
else if (spcoldep <= 16) {
shptr = (short*)&sprit->GetScanLine(rr)[0];
shptr2 = (short*)&checkPixelsFrom->GetScanLine((rr * 100) / zoom)[0];
if (shptr2[(ee * 100) / zoom] != maskcol) {
shptr[ee] = ((short*)(&copyPixelsFrom->GetScanLine(rr + yy)[0]))[ee + xx];
pixelsChanged = 1;
}
}
else if (spcoldep == 24) {
char *chptr = (char*)&sprit->GetScanLine(rr)[0];
char *chptr2 = (char*)&checkPixelsFrom->GetScanLine((rr * 100) / zoom)[0];
if (memcmp(&chptr2[((ee * 100) / zoom) * 3], &maskcol, 3) != 0) {
memcpy(&chptr[ee * 3], &copyPixelsFrom->GetScanLine(rr + yy)[(ee + xx) * 3], 3);
pixelsChanged = 1;
}
}
else if (spcoldep <= 32) {
loptr = (int*)&sprit->GetScanLine(rr)[0];
loptr2 = (int*)&checkPixelsFrom->GetScanLine((rr * 100) / zoom)[0];
if (loptr2[(ee * 100) / zoom] != maskcol) {
loptr[ee] = ((int*)(&copyPixelsFrom->GetScanLine(rr + yy)[0]))[ee + xx];
pixelsChanged = 1;
}
}
}
else
{
pixelsChanged = 1;
if (spcoldep <= 8)
sprit->GetScanLineForWriting(rr)[ee] = maskcol;
else if (spcoldep <= 16) {
shptr = (short*)&sprit->GetScanLine(rr)[0];
shptr[ee] = maskcol;
}
else if (spcoldep == 24) {
char *chptr = (char*)&sprit->GetScanLine(rr)[0];
memcpy(&chptr[ee * 3], &maskcol, 3);
}
else if (spcoldep <= 32) {
loptr = (int*)&sprit->GetScanLine(rr)[0];
loptr[ee] = maskcol;
}
else
quit("!Sprite colour depth >32 ??");
}
}
}
return pixelsChanged;
}
void sort_out_char_sprite_walk_behind(int actspsIndex, int xx, int yy, int basel, int zoom, int width, int height)
{
if (noWalkBehindsAtAll)
return;
if ((!actspswbcache[actspsIndex].valid) ||
(actspswbcache[actspsIndex].xWas != xx) ||
(actspswbcache[actspsIndex].yWas != yy) ||
(actspswbcache[actspsIndex].baselineWas != basel))
{
actspswb[actspsIndex] = recycle_bitmap(actspswb[actspsIndex], thisroom.BgFrames[play.bg_frame].Graphic->GetColorDepth(), width, height, true);
Bitmap *wbSprite = actspswb[actspsIndex];
actspswbcache[actspsIndex].isWalkBehindHere = sort_out_walk_behinds(wbSprite, xx, yy, basel, thisroom.BgFrames[play.bg_frame].Graphic.get(), actsps[actspsIndex], zoom);
actspswbcache[actspsIndex].xWas = xx;
actspswbcache[actspsIndex].yWas = yy;
actspswbcache[actspsIndex].baselineWas = basel;
actspswbcache[actspsIndex].valid = 1;
if (actspswbcache[actspsIndex].isWalkBehindHere)
{
actspswbbmp[actspsIndex] = recycle_ddb_bitmap(actspswbbmp[actspsIndex], actspswb[actspsIndex], false);
}
}
if (actspswbcache[actspsIndex].isWalkBehindHere)
{
add_to_sprite_list(actspswbbmp[actspsIndex], xx, yy, basel, 0, -1, true);
}
}
void clear_draw_list() {
thingsToDrawList.clear();
}
void add_thing_to_draw(IDriverDependantBitmap* bmp, int x, int y, int trans, bool alphaChannel) {
SpriteListEntry sprite;
sprite.pic = nullptr;
sprite.bmp = bmp;
sprite.x = x;
sprite.y = y;
sprite.transparent = trans;
sprite.hasAlphaChannel = alphaChannel;
thingsToDrawList.push_back(sprite);
}
// the sprite list is an intermediate list used to order
// objects and characters by their baselines before everything
// is added to the Thing To Draw List
void clear_sprite_list() {
sprlist.clear();
}
void add_to_sprite_list(IDriverDependantBitmap* spp, int xx, int yy, int baseline, int trans, int sprNum, bool isWalkBehind) {
if (spp == nullptr)
quit("add_to_sprite_list: attempted to draw NULL sprite");
// completely invisible, so don't draw it at all
if (trans == 255)
return;
SpriteListEntry sprite;
if ((sprNum >= 0) && ((game.SpriteInfos[sprNum].Flags & SPF_ALPHACHANNEL) != 0))
sprite.hasAlphaChannel = true;
else
sprite.hasAlphaChannel = false;
sprite.bmp = spp;
sprite.baseline = baseline;
sprite.x=xx;
sprite.y=yy;
sprite.transparent=trans;
if (walkBehindMethod == DrawAsSeparateSprite)
sprite.takesPriorityIfEqual = !isWalkBehind;
else
sprite.takesPriorityIfEqual = isWalkBehind;
sprlist.push_back(sprite);
}
void repair_alpha_channel(Bitmap *dest, Bitmap *bgpic)
{
// Repair the alpha channel, because sprites may have been drawn
// over it by the buttons, etc
int theWid = (dest->GetWidth() < bgpic->GetWidth()) ? dest->GetWidth() : bgpic->GetWidth();
int theHit = (dest->GetHeight() < bgpic->GetHeight()) ? dest->GetHeight() : bgpic->GetHeight();
for (int y = 0; y < theHit; y++)
{
unsigned int *destination = ((unsigned int*)dest->GetScanLineForWriting(y));
unsigned int *source = ((unsigned int*)bgpic->GetScanLineForWriting(y));
for (int x = 0; x < theWid; x++)
{
destination[x] |= (source[x] & 0xff000000);
}
}
}
// used by GUI renderer to draw images
void draw_gui_sprite(Bitmap *ds, int pic, int x, int y, bool use_alpha, BlendMode blend_mode)
{
Bitmap *sprite = spriteset[pic];
const bool ds_has_alpha = ds->GetColorDepth() == 32;
const bool src_has_alpha = (game.SpriteInfos[pic].Flags & SPF_ALPHACHANNEL) != 0;
if (use_alpha && game.options[OPT_NEWGUIALPHA] == kGuiAlphaRender_Proper)
{
GfxUtil::DrawSpriteBlend(ds, Point(x, y), sprite, blend_mode, ds_has_alpha, src_has_alpha);
}
// Backwards-compatible drawing
else if (use_alpha && ds_has_alpha && game.options[OPT_NEWGUIALPHA] == kGuiAlphaRender_AdditiveAlpha)
{
if (src_has_alpha)
set_additive_alpha_blender();
else
set_opaque_alpha_blender();
ds->TransBlendBlt(sprite, x, y);
}
else
{
GfxUtil::DrawSpriteWithTransparency(ds, sprite, x, y);
}
}
void draw_gui_sprite_v330(Bitmap *ds, int pic, int x, int y, bool use_alpha, BlendMode blend_mode)
{
draw_gui_sprite(ds, pic, x, y, use_alpha && (loaded_game_file_version >= kGameVersion_330), blend_mode);
}
// function to sort the sprites into baseline order
bool spritelistentry_less(const SpriteListEntry &e1, const SpriteListEntry &e2)
{
if (e1.baseline == e2.baseline)
{
if (e1.takesPriorityIfEqual)
return false;
if (e2.takesPriorityIfEqual)
return true;
}
return e1.baseline < e2.baseline;
}
void draw_sprite_list() {
if (walkBehindMethod == DrawAsSeparateSprite)
{
for (int ee = 1; ee < MAX_WALK_BEHINDS; ee++)
{
if (walkBehindBitmap[ee] != nullptr)
{
add_to_sprite_list(walkBehindBitmap[ee], walkBehindLeft[ee], walkBehindTop[ee],
croom->walkbehind_base[ee], 0, -1, true);
}
}
}
std::sort(sprlist.begin(), sprlist.end(), spritelistentry_less);
if(pl_any_want_hook(AGSE_PRESCREENDRAW))
add_thing_to_draw(nullptr, AGSE_PRESCREENDRAW, 0, TRANS_RUN_PLUGIN, false);
// copy the sorted sprites into the Things To Draw list
thingsToDrawList.insert(thingsToDrawList.end(), sprlist.begin(), sprlist.end());
}
// Avoid freeing and reallocating the memory if possible
Bitmap *recycle_bitmap(Bitmap *bimp, int coldep, int wid, int hit, bool make_transparent) {
if (bimp != nullptr) {
// same colour depth, width and height -> reuse
if ((bimp->GetColorDepth() == coldep) && (bimp->GetWidth() == wid)
&& (bimp->GetHeight() == hit))
{
if (make_transparent)
{
bimp->ClearTransparent();
}
return bimp;
}
delete bimp;
}
bimp = make_transparent ? BitmapHelper::CreateTransparentBitmap(wid, hit,coldep) :
BitmapHelper::CreateBitmap(wid, hit,coldep);
return bimp;
}
// Get the local tint at the specified X & Y co-ordinates, based on
// room regions and SetAmbientTint
// tint_amnt will be set to 0 if there is no tint enabled
// if this is the case, then light_lev holds the light level (0=none)
void get_local_tint(int xpp, int ypp, int nolight,
int *tint_amnt, int *tint_r, int *tint_g,
int *tint_b, int *tint_lit,
int *light_lev) {
int tint_level = 0, light_level = 0;
int tint_amount = 0;
int tint_red = 0;
int tint_green = 0;
int tint_blue = 0;
int tint_light = 255;
if (nolight == 0) {
int onRegion = 0;
if ((play.ground_level_areas_disabled & GLED_EFFECTS) == 0) {
// check if the player is on a region, to find its
// light/tint level
onRegion = GetRegionIDAtRoom(xpp, ypp);
if (onRegion == 0) {
// when walking, he might just be off a walkable area
onRegion = GetRegionIDAtRoom(xpp - 3, ypp);
if (onRegion == 0)
onRegion = GetRegionIDAtRoom(xpp + 3, ypp);
if (onRegion == 0)
onRegion = GetRegionIDAtRoom(xpp, ypp - 3);
if (onRegion == 0)
onRegion = GetRegionIDAtRoom(xpp, ypp + 3);
}
}
if ((onRegion > 0) && (onRegion < MAX_ROOM_REGIONS)) {
light_level = thisroom.Regions[onRegion].Light;
tint_level = thisroom.Regions[onRegion].Tint;
}
else if (onRegion <= 0) {
light_level = thisroom.Regions[0].Light;
tint_level = thisroom.Regions[0].Tint;
}
int tint_sat = (tint_level >> 24) & 0xFF;
if ((game.color_depth == 1) || ((tint_level & 0x00ffffff) == 0) ||
(tint_sat == 0))
tint_level = 0;
if (tint_level) {
tint_red = (unsigned char)(tint_level & 0x000ff);
tint_green = (unsigned char)((tint_level >> 8) & 0x000ff);
tint_blue = (unsigned char)((tint_level >> 16) & 0x000ff);
tint_amount = tint_sat;
tint_light = light_level;
}
if (play.rtint_enabled)
{
if (play.rtint_level > 0)
{
// override with room tint
tint_red = play.rtint_red;
tint_green = play.rtint_green;
tint_blue = play.rtint_blue;
tint_amount = play.rtint_level;
tint_light = play.rtint_light;
}
else
{
// override with room light level
tint_amount = 0;
light_level = play.rtint_light;
}
}
}
// copy to output parameters
*tint_amnt = tint_amount;
*tint_r = tint_red;
*tint_g = tint_green;
*tint_b = tint_blue;
*tint_lit = tint_light;
if (light_lev)
*light_lev = light_level;
}
// Applies the specified RGB Tint or Light Level to the actsps
// sprite indexed with actspsindex
void apply_tint_or_light(int actspsindex, int light_level,
int tint_amount, int tint_red, int tint_green,
int tint_blue, int tint_light, int coldept,
Bitmap *blitFrom) {
// In a 256-colour game, we cannot do tinting or lightening
// (but we can do darkening, if light_level < 0)
if (game.color_depth == 1) {
if ((light_level > 0) || (tint_amount != 0))
return;
}
// we can only do tint/light if the colour depths match
if (game.GetColorDepth() == actsps[actspsindex]->GetColorDepth()) {
Bitmap *oldwas;
// if the caller supplied a source bitmap, ->Blit from it
// (used as a speed optimisation where possible)
if (blitFrom)
oldwas = blitFrom;
// otherwise, make a new target bmp
else {
oldwas = actsps[actspsindex];
actsps[actspsindex] = BitmapHelper::CreateBitmap(oldwas->GetWidth(), oldwas->GetHeight(), coldept);
}
Bitmap *active_spr = actsps[actspsindex];
if (tint_amount) {
// It is an RGB tint
tint_image (active_spr, oldwas, tint_red, tint_green, tint_blue, tint_amount, tint_light);
}
else {
// the RGB values passed to set_trans_blender decide whether it will darken
// or lighten sprites ( <128=darken, >128=lighten). The parameter passed
// to LitBlendBlt defines how much it will be darkened/lightened by.
int lit_amnt;
active_spr->FillTransparent();
// It's a light level, not a tint
if (game.color_depth == 1) {
// 256-col
lit_amnt = (250 - ((-light_level) * 5)/2);
}
else {
// hi-color
if (light_level < 0)
set_my_trans_blender(8,8,8,0);
else
set_my_trans_blender(248,248,248,0);
lit_amnt = abs(light_level) * 2;
}
active_spr->LitBlendBlt(oldwas, 0, 0, lit_amnt);
}
if (oldwas != blitFrom)
delete oldwas;
}
else if (blitFrom) {
// sprite colour depth != game colour depth, so don't try and tint
// but we do need to do something, so copy the source
Bitmap *active_spr = actsps[actspsindex];
active_spr->Blit(blitFrom, 0, 0, 0, 0, active_spr->GetWidth(), active_spr->GetHeight());
}
}
// Draws the specified 'sppic' sprite onto actsps[useindx] at the
// specified width and height, and flips the sprite if necessary.
// Returns 1 if something was drawn to actsps; returns 0 if no
// scaling or stretching was required, in which case nothing was done
int scale_and_flip_sprite(int useindx, int coldept, int zoom_level,
int sppic, int newwidth, int newheight,
int isMirrored) {
int actsps_used = 1;
// create and blank out the new sprite
actsps[useindx] = recycle_bitmap(actsps[useindx], coldept, newwidth, newheight, true);
Bitmap *active_spr = actsps[useindx];
if (zoom_level != 100) {
// Scaled character
our_eip = 334;
// Ensure that anti-aliasing routines have a palette to
// use for mapping while faded out
if (in_new_room)
select_palette (palette);
if (isMirrored) {
Bitmap *tempspr = BitmapHelper::CreateBitmap(newwidth, newheight,coldept);
tempspr->Fill (actsps[useindx]->GetMaskColor());
if ((IS_ANTIALIAS_SPRITES) && ((game.SpriteInfos[sppic].Flags & SPF_ALPHACHANNEL) == 0))
tempspr->AAStretchBlt (spriteset[sppic], RectWH(0, 0, newwidth, newheight), Common::kBitmap_Transparency);
else
tempspr->StretchBlt (spriteset[sppic], RectWH(0, 0, newwidth, newheight), Common::kBitmap_Transparency);
active_spr->FlipBlt(tempspr, 0, 0, Common::kBitmap_HFlip);
delete tempspr;
}
else if ((IS_ANTIALIAS_SPRITES) && ((game.SpriteInfos[sppic].Flags & SPF_ALPHACHANNEL) == 0))
active_spr->AAStretchBlt(spriteset[sppic],RectWH(0,0,newwidth,newheight), Common::kBitmap_Transparency);
else
active_spr->StretchBlt(spriteset[sppic],RectWH(0,0,newwidth,newheight), Common::kBitmap_Transparency);
/* AASTR2 version of code (doesn't work properly, gives black borders)
if (IS_ANTIALIAS_SPRITES) {
int aa_mode = AA_MASKED;
if (game.spriteflags[sppic] & SPF_ALPHACHANNEL)
aa_mode |= AA_ALPHA | AA_RAW_ALPHA;
if (isMirrored)
aa_mode |= AA_HFLIP;
aa_set_mode(aa_mode);
->AAStretchBlt(actsps[useindx],spriteset[sppic],0,0,newwidth,newheight);
}
else if (isMirrored) {
Bitmap *tempspr = BitmapHelper::CreateBitmap_ (coldept, newwidth, newheight);
->Clear (tempspr, ->GetMaskColor(actsps[useindx]));
->StretchBlt (tempspr, spriteset[sppic], 0, 0, newwidth, newheight);
->FlipBlt(Common::kBitmap_HFlip, (actsps[useindx], tempspr, 0, 0);
wfreeblock (tempspr);
}
else
->StretchBlt(actsps[useindx],spriteset[sppic],0,0,newwidth,newheight);
*/
if (in_new_room)
unselect_palette();
}
else {
// Not a scaled character, draw at normal size
our_eip = 339;
if (isMirrored)
active_spr->FlipBlt(spriteset[sppic], 0, 0, Common::kBitmap_HFlip);
else
actsps_used = 0;
//->Blit (spriteset[sppic], actsps[useindx], 0, 0, 0, 0, actsps[useindx]->GetWidth(), actsps[useindx]->GetHeight());
}
return actsps_used;
}
// create the actsps[aa] image with the object drawn correctly
// returns 1 if nothing at all has changed and actsps is still
// intact from last time; 0 otherwise
int construct_object_gfx(int aa, int *drawnWidth, int *drawnHeight, bool alwaysUseSoftware) {
int useindx = aa;
bool hardwareAccelerated = !alwaysUseSoftware && gfxDriver->HasAcceleratedTransform();
if (spriteset[objs[aa].num] == nullptr)
quitprintf("There was an error drawing object %d. Its current sprite, %d, is invalid.", aa, objs[aa].num);
int coldept = spriteset[objs[aa].num]->GetColorDepth();
int sprwidth = game.SpriteInfos[objs[aa].num].Width;
int sprheight = game.SpriteInfos[objs[aa].num].Height;
int tint_red, tint_green, tint_blue;
int tint_level, tint_light, light_level;
int zoom_level = 100;
// calculate the zoom level
if ((objs[aa].flags & OBJF_USEROOMSCALING) == 0)
{
zoom_level = objs[aa].zoom;
}
else
{
int onarea = get_walkable_area_at_location(objs[aa].x, objs[aa].y);
if ((onarea <= 0) && (thisroom.WalkAreas[0].ScalingFar == 0)) {
// just off the edge of an area -- use the scaling we had
// while on the area
zoom_level = objs[aa].zoom;
}
else
zoom_level = get_area_scaling(onarea, objs[aa].x, objs[aa].y);
}
if (zoom_level != 100)
scale_sprite_size(objs[aa].num, zoom_level, &sprwidth, &sprheight);
objs[aa].zoom = zoom_level;
// save width/height into parameters if requested
if (drawnWidth)
*drawnWidth = sprwidth;
if (drawnHeight)
*drawnHeight = sprheight;
objs[aa].last_width = sprwidth;
objs[aa].last_height = sprheight;
tint_red = tint_green = tint_blue = tint_level = tint_light = light_level = 0;
if (objs[aa].flags & OBJF_HASTINT) {
// object specific tint, use it
tint_red = objs[aa].tint_r;
tint_green = objs[aa].tint_g;
tint_blue = objs[aa].tint_b;
tint_level = objs[aa].tint_level;
tint_light = objs[aa].tint_light;
light_level = 0;
}
else if (objs[aa].flags & OBJF_HASLIGHT)
{
light_level = objs[aa].tint_light;
}
else {
// get the ambient or region tint
int ignoreRegionTints = 1;
if (objs[aa].flags & OBJF_USEREGIONTINTS)
ignoreRegionTints = 0;
get_local_tint(objs[aa].x, objs[aa].y, ignoreRegionTints,
&tint_level, &tint_red, &tint_green, &tint_blue,
&tint_light, &light_level);
}
// check whether the image should be flipped
int isMirrored = 0;
if ( (objs[aa].view >= 0) &&
(views[objs[aa].view].loops[objs[aa].loop].frames[objs[aa].frame].pic == objs[aa].num) &&
((views[objs[aa].view].loops[objs[aa].loop].frames[objs[aa].frame].flags & VFLG_FLIPSPRITE) != 0)) {
isMirrored = 1;
}
if ((hardwareAccelerated) &&
(walkBehindMethod != DrawOverCharSprite) &&
(objcache[aa].image != nullptr) &&
(objcache[aa].sppic == objs[aa].num) &&
(actsps[useindx] != nullptr))
{
// HW acceleration
objcache[aa].tintamntwas = tint_level;
objcache[aa].tintredwas = tint_red;
objcache[aa].tintgrnwas = tint_green;
objcache[aa].tintbluwas = tint_blue;
objcache[aa].tintlightwas = tint_light;
objcache[aa].lightlevwas = light_level;
objcache[aa].zoomWas = zoom_level;
objcache[aa].mirroredWas = isMirrored;
return 1;
}
if ((!hardwareAccelerated) && (gfxDriver->HasAcceleratedTransform()))
{
// They want to draw it in software mode with the D3D driver,
// so force a redraw
objcache[aa].sppic = -389538;
}
// If we have the image cached, use it
if ((objcache[aa].image != nullptr) &&
(objcache[aa].sppic == objs[aa].num) &&
(objcache[aa].tintamntwas == tint_level) &&
(objcache[aa].tintlightwas == tint_light) &&
(objcache[aa].tintredwas == tint_red) &&
(objcache[aa].tintgrnwas == tint_green) &&
(objcache[aa].tintbluwas == tint_blue) &&
(objcache[aa].lightlevwas == light_level) &&
(objcache[aa].zoomWas == zoom_level) &&
(objcache[aa].mirroredWas == isMirrored)) {
// the image is the same, we can use it cached!
if ((walkBehindMethod != DrawOverCharSprite) &&
(actsps[useindx] != nullptr))
return 1;
// Check if the X & Y co-ords are the same, too -- if so, there
// is scope for further optimisations
if ((objcache[aa].xwas == objs[aa].x) &&
(objcache[aa].ywas == objs[aa].y) &&
(actsps[useindx] != nullptr) &&
(walk_behind_baselines_changed == 0))
return 1;
actsps[useindx] = recycle_bitmap(actsps[useindx], coldept, sprwidth, sprheight);
actsps[useindx]->Blit(objcache[aa].image, 0, 0, 0, 0, objcache[aa].image->GetWidth(), objcache[aa].image->GetHeight());
return 0;
}
// Not cached, so draw the image
int actspsUsed = 0;
if (!hardwareAccelerated)
{
// draw the base sprite, scaled and flipped as appropriate
actspsUsed = scale_and_flip_sprite(useindx, coldept, zoom_level,
objs[aa].num, sprwidth, sprheight, isMirrored);
}
else
{
// ensure actsps exists
actsps[useindx] = recycle_bitmap(actsps[useindx], coldept, game.SpriteInfos[objs[aa].num].Width, game.SpriteInfos[objs[aa].num].Height);
}
// direct read from source bitmap, where possible
Bitmap *comeFrom = nullptr;
if (!actspsUsed)
comeFrom = spriteset[objs[aa].num];
// apply tints or lightenings where appropriate, else just copy
// the source bitmap
if (!hardwareAccelerated && ((tint_level > 0) || (light_level != 0)))
{
apply_tint_or_light(useindx, light_level, tint_level, tint_red,
tint_green, tint_blue, tint_light, coldept,
comeFrom);
}
else if (!actspsUsed) {
actsps[useindx]->Blit(spriteset[objs[aa].num],0,0,0,0,game.SpriteInfos[objs[aa].num].Width, game.SpriteInfos[objs[aa].num].Height);
}
// Re-use the bitmap if it's the same size
objcache[aa].image = recycle_bitmap(objcache[aa].image, coldept, sprwidth, sprheight);
// Create the cached image and store it
objcache[aa].image->Blit(actsps[useindx], 0, 0, 0, 0, sprwidth, sprheight);
objcache[aa].sppic = objs[aa].num;
objcache[aa].tintamntwas = tint_level;
objcache[aa].tintredwas = tint_red;
objcache[aa].tintgrnwas = tint_green;
objcache[aa].tintbluwas = tint_blue;
objcache[aa].tintlightwas = tint_light;
objcache[aa].lightlevwas = light_level;
objcache[aa].zoomWas = zoom_level;
objcache[aa].mirroredWas = isMirrored;
return 0;
}
// This is only called from draw_screen_background, but it's seperated
// to help with profiling the program
void prepare_objects_for_drawing() {
our_eip=32;
for (int aa=0; aa<croom->numobj; aa++) {
if (objs[aa].on != 1) continue;
// offscreen, don't draw
if ((objs[aa].x >= thisroom.Width) || (objs[aa].y < 1))
continue;
const int useindx = aa;
int tehHeight;
int actspsIntact = construct_object_gfx(aa, nullptr, &tehHeight, false);
// update the cache for next time
objcache[aa].xwas = objs[aa].x;
objcache[aa].ywas = objs[aa].y;
int atxp = data_to_game_coord(objs[aa].x);
int atyp = data_to_game_coord(objs[aa].y) - tehHeight;
int usebasel = objs[aa].get_baseline();
if (objs[aa].flags & OBJF_NOWALKBEHINDS) {
// ignore walk-behinds, do nothing
if (walkBehindMethod == DrawAsSeparateSprite)
{
usebasel += thisroom.Height;
}
}
else if (walkBehindMethod == DrawAsSeparateCharSprite)
{
sort_out_char_sprite_walk_behind(useindx, atxp, atyp, usebasel, objs[aa].zoom, objs[aa].last_width, objs[aa].last_height);
}
else if ((!actspsIntact) && (walkBehindMethod == DrawOverCharSprite))
{
sort_out_walk_behinds(actsps[useindx], atxp, atyp, usebasel);
}
if ((!actspsIntact) || (actspsbmp[useindx] == nullptr))
{
bool hasAlpha = (game.SpriteInfos[objs[aa].num].Flags & SPF_ALPHACHANNEL) != 0;
if (actspsbmp[useindx] != nullptr)
gfxDriver->DestroyDDB(actspsbmp[useindx]);
actspsbmp[useindx] = gfxDriver->CreateDDBFromBitmap(actsps[useindx], hasAlpha);
}
if (gfxDriver->HasAcceleratedTransform())
{
actspsbmp[useindx]->SetFlippedLeftRight(objcache[aa].mirroredWas != 0);
actspsbmp[useindx]->SetStretch(objs[aa].last_width, objs[aa].last_height);
actspsbmp[useindx]->SetTint(objcache[aa].tintredwas, objcache[aa].tintgrnwas, objcache[aa].tintbluwas, (objcache[aa].tintamntwas * 256) / 100);
if (objcache[aa].tintamntwas > 0)
{
if (objcache[aa].tintlightwas == 0) // luminance of 0 -- pass 1 to enable
actspsbmp[useindx]->SetLightLevel(1);
else if (objcache[aa].tintlightwas < 250)
actspsbmp[useindx]->SetLightLevel(objcache[aa].tintlightwas);
else
actspsbmp[useindx]->SetLightLevel(0);
}
else if (objcache[aa].lightlevwas != 0)
actspsbmp[useindx]->SetLightLevel((objcache[aa].lightlevwas * 25) / 10 + 256);
else
actspsbmp[useindx]->SetLightLevel(0);
}
add_to_sprite_list(actspsbmp[useindx], atxp, atyp, usebasel, objs[aa].transparent,objs[aa].num);
}
}
// Draws srcimg onto destimg, tinting to the specified level
// Totally overwrites the contents of the destination image
void tint_image (Bitmap *ds, Bitmap *srcimg, int red, int grn, int blu, int light_level, int luminance) {
if ((srcimg->GetColorDepth() != ds->GetColorDepth()) ||
(srcimg->GetColorDepth() <= 8)) {
debug_script_warn("Image tint failed - images must both be hi-color");
// the caller expects something to have been copied
ds->Blit(srcimg, 0, 0, 0, 0, srcimg->GetWidth(), srcimg->GetHeight());
return;
}
// For performance reasons, we have a seperate blender for
// when light is being adjusted and when it is not.
// If luminance >= 250, then normal brightness, otherwise darken
if (luminance >= 250)
set_blender_mode (_myblender_color15, _myblender_color16, _myblender_color32, red, grn, blu, 0);
else
set_blender_mode (_myblender_color15_light, _myblender_color16_light, _myblender_color32_light, red, grn, blu, 0);
if (light_level >= 100) {
// fully colourised
ds->FillTransparent();
ds->LitBlendBlt(srcimg, 0, 0, luminance);
}
else {
// light_level is between -100 and 100 normally; 0-100 in
// this case when it's a RGB tint
light_level = (light_level * 25) / 10;
// Copy the image to the new bitmap
ds->Blit(srcimg, 0, 0, 0, 0, srcimg->GetWidth(), srcimg->GetHeight());
// Render the colourised image to a temporary bitmap,
// then transparently draw it over the original image
Bitmap *finaltarget = BitmapHelper::CreateTransparentBitmap(srcimg->GetWidth(), srcimg->GetHeight(), srcimg->GetColorDepth());
finaltarget->LitBlendBlt(srcimg, 0, 0, luminance);
// customized trans blender to preserve alpha channel
set_my_trans_blender (0, 0, 0, light_level);
ds->TransBlendBlt (finaltarget, 0, 0);
delete finaltarget;
}
}
void prepare_characters_for_drawing() {
int zoom_level,newwidth,newheight,onarea,sppic;
int light_level,coldept;
int tint_red, tint_green, tint_blue, tint_amount, tint_light = 255;
our_eip=33;
// draw characters
for (int aa=0; aa < game.numcharacters; aa++) {
if (game.chars[aa].on==0) continue;
if (game.chars[aa].room!=displayed_room) continue;
eip_guinum = aa;
const int useindx = aa + MAX_ROOM_OBJECTS;
CharacterInfo*chin=&game.chars[aa];
our_eip = 330;
// if it's on but set to view -1, they're being silly
if (chin->view < 0) {
quitprintf("!The character '%s' was turned on in the current room (room %d) but has not been assigned a view number.",
chin->name, displayed_room);
}
if (chin->frame >= views[chin->view].loops[chin->loop].numFrames)
chin->frame = 0;
if ((chin->loop >= views[chin->view].numLoops) ||
(views[chin->view].loops[chin->loop].numFrames < 1)) {
quitprintf("!The character '%s' could not be displayed because there were no frames in loop %d of view %d.",
chin->name, chin->loop, chin->view + 1);
}
sppic=views[chin->view].loops[chin->loop].frames[chin->frame].pic;
if (sppic < 0)
sppic = 0; // in case it's screwed up somehow
our_eip = 331;
// sort out the stretching if required
onarea = get_walkable_area_at_character (aa);
our_eip = 332;
// calculate the zoom level
if (chin->flags & CHF_MANUALSCALING) // character ignores scaling
zoom_level = charextra[aa].zoom;
else if ((onarea <= 0) && (thisroom.WalkAreas[0].ScalingFar == 0)) {
zoom_level = charextra[aa].zoom;
// NOTE: room objects don't have this fix
if (zoom_level == 0)
zoom_level = 100;
}
else
zoom_level = get_area_scaling (onarea, chin->x, chin->y);
charextra[aa].zoom = zoom_level;
tint_red = tint_green = tint_blue = tint_amount = tint_light = light_level = 0;
if (chin->flags & CHF_HASTINT) {
// object specific tint, use it
tint_red = charextra[aa].tint_r;
tint_green = charextra[aa].tint_g;
tint_blue = charextra[aa].tint_b;
tint_amount = charextra[aa].tint_level;
tint_light = charextra[aa].tint_light;
light_level = 0;
}
else if (chin->flags & CHF_HASLIGHT)
{
light_level = charextra[aa].tint_light;
}
else {
get_local_tint(chin->x, chin->y, chin->flags & CHF_NOLIGHTING,
&tint_amount, &tint_red, &tint_green, &tint_blue,
&tint_light, &light_level);
}
our_eip = 3330;
int isMirrored = 0, specialpic = sppic;
bool usingCachedImage = false;
coldept = spriteset[sppic]->GetColorDepth();
// adjust the sppic if mirrored, so it doesn't accidentally
// cache the mirrored frame as the real one
if (views[chin->view].loops[chin->loop].frames[chin->frame].flags & VFLG_FLIPSPRITE) {
isMirrored = 1;
specialpic = -sppic;
}
our_eip = 3331;
// if the character was the same sprite and scaling last time,
// just use the cached image
if ((charcache[aa].inUse) &&
(charcache[aa].sppic == specialpic) &&
(charcache[aa].scaling == zoom_level) &&
(charcache[aa].tintredwas == tint_red) &&
(charcache[aa].tintgrnwas == tint_green) &&
(charcache[aa].tintbluwas == tint_blue) &&
(charcache[aa].tintamntwas == tint_amount) &&
(charcache[aa].tintlightwas == tint_light) &&
(charcache[aa].lightlevwas == light_level))
{
if (walkBehindMethod == DrawOverCharSprite)
{
actsps[useindx] = recycle_bitmap(actsps[useindx], charcache[aa].image->GetColorDepth(), charcache[aa].image->GetWidth(), charcache[aa].image->GetHeight());
actsps[useindx]->Blit (charcache[aa].image, 0, 0, 0, 0, actsps[useindx]->GetWidth(), actsps[useindx]->GetHeight());
}
else
{
usingCachedImage = true;
}
}
else if ((charcache[aa].inUse) &&
(charcache[aa].sppic == specialpic) &&
(gfxDriver->HasAcceleratedTransform()))
{
usingCachedImage = true;
}
else if (charcache[aa].inUse) {
//destroy_bitmap (charcache[aa].image);
charcache[aa].inUse = 0;
}
our_eip = 3332;
if (zoom_level != 100) {
// it needs to be stretched, so calculate the new dimensions
scale_sprite_size(sppic, zoom_level, &newwidth, &newheight);
charextra[aa].width=newwidth;
charextra[aa].height=newheight;
}
else {
// draw at original size, so just use the sprite width and height
// TODO: store width and height always, that's much simplier to use for reference!
charextra[aa].width=0;
charextra[aa].height=0;
newwidth = game.SpriteInfos[sppic].Width;
newheight = game.SpriteInfos[sppic].Height;
}
our_eip = 3336;
// Calculate the X & Y co-ordinates of where the sprite will be
const int atxp =(data_to_game_coord(chin->x)) - newwidth/2;
const int atyp =(data_to_game_coord(chin->y) - newheight)
// adjust the Y positioning for the character's Z co-ord
- data_to_game_coord(chin->z);
charcache[aa].scaling = zoom_level;
charcache[aa].sppic = specialpic;
charcache[aa].tintredwas = tint_red;
charcache[aa].tintgrnwas = tint_green;
charcache[aa].tintbluwas = tint_blue;
charcache[aa].tintamntwas = tint_amount;
charcache[aa].tintlightwas = tint_light;
charcache[aa].lightlevwas = light_level;
// If cache needs to be re-drawn
if (!charcache[aa].inUse) {
// create the base sprite in actsps[useindx], which will
// be scaled and/or flipped, as appropriate
int actspsUsed = 0;
if (!gfxDriver->HasAcceleratedTransform())
{
actspsUsed = scale_and_flip_sprite(
useindx, coldept, zoom_level, sppic,
newwidth, newheight, isMirrored);
}
else
{
// ensure actsps exists
actsps[useindx] = recycle_bitmap(actsps[useindx], coldept, game.SpriteInfos[sppic].Width, game.SpriteInfos[sppic].Height);
}
our_eip = 335;
if (((light_level != 0) || (tint_amount != 0)) &&
(!gfxDriver->HasAcceleratedTransform())) {
// apply the lightening or tinting
Bitmap *comeFrom = nullptr;
// if possible, direct read from the source image
if (!actspsUsed)
comeFrom = spriteset[sppic];
apply_tint_or_light(useindx, light_level, tint_amount, tint_red,
tint_green, tint_blue, tint_light, coldept,
comeFrom);
}
else if (!actspsUsed) {
// no scaling, flipping or tinting was done, so just blit it normally
actsps[useindx]->Blit (spriteset[sppic], 0, 0, 0, 0, actsps[useindx]->GetWidth(), actsps[useindx]->GetHeight());
}
// update the character cache with the new image
charcache[aa].inUse = 1;
//charcache[aa].image = BitmapHelper::CreateBitmap_ (coldept, actsps[useindx]->GetWidth(), actsps[useindx]->GetHeight());
charcache[aa].image = recycle_bitmap(charcache[aa].image, coldept, actsps[useindx]->GetWidth(), actsps[useindx]->GetHeight());
charcache[aa].image->Blit (actsps[useindx], 0, 0, 0, 0, actsps[useindx]->GetWidth(), actsps[useindx]->GetHeight());
} // end if !cache.inUse
int usebasel = chin->get_baseline();
our_eip = 336;
const int bgX = atxp + chin->pic_xoffs;
const int bgY = atyp + chin->pic_yoffs;
if (chin->flags & CHF_NOWALKBEHINDS) {
// ignore walk-behinds, do nothing
if (walkBehindMethod == DrawAsSeparateSprite)
{
usebasel += thisroom.Height;
}
}
else if (walkBehindMethod == DrawAsSeparateCharSprite)
{
sort_out_char_sprite_walk_behind(useindx, bgX, bgY, usebasel, charextra[aa].zoom, newwidth, newheight);
}
else if (walkBehindMethod == DrawOverCharSprite)
{
sort_out_walk_behinds(actsps[useindx], bgX, bgY, usebasel);
}
if ((!usingCachedImage) || (actspsbmp[useindx] == nullptr))
{
bool hasAlpha = (game.SpriteInfos[sppic].Flags & SPF_ALPHACHANNEL) != 0;
actspsbmp[useindx] = recycle_ddb_bitmap(actspsbmp[useindx], actsps[useindx], hasAlpha);
}
if (gfxDriver->HasAcceleratedTransform())
{
actspsbmp[useindx]->SetStretch(newwidth, newheight);
actspsbmp[useindx]->SetFlippedLeftRight(isMirrored != 0);
actspsbmp[useindx]->SetTint(tint_red, tint_green, tint_blue, (tint_amount * 256) / 100);
if (tint_amount != 0)
{
if (tint_light == 0) // tint with 0 luminance, pass as 1 instead
actspsbmp[useindx]->SetLightLevel(1);
else if (tint_light < 250)
actspsbmp[useindx]->SetLightLevel(tint_light);
else
actspsbmp[useindx]->SetLightLevel(0);
}
else if (light_level != 0)
actspsbmp[useindx]->SetLightLevel((light_level * 25) / 10 + 256);
else
actspsbmp[useindx]->SetLightLevel(0);
}
our_eip = 337;
chin->actx = atxp;
chin->acty = atyp;
add_to_sprite_list(actspsbmp[useindx], bgX, bgY, usebasel, chin->transparency, sppic);
}
}
// Compiles a list of room sprites (characters, objects, background)
void prepare_room_sprites()
{
// Background sprite is required for the non-software renderers always,
// and for software renderer in case there are overlapping viewports.
// Note that software DDB is just a tiny wrapper around bitmap, so overhead is negligible.
if (roomBackgroundBmp == nullptr)
{
update_polled_stuff_if_runtime();
roomBackgroundBmp = gfxDriver->CreateDDBFromBitmap(thisroom.BgFrames[play.bg_frame].Graphic.get(), false, true);
}
else if (current_background_is_dirty)
{
update_polled_stuff_if_runtime();
gfxDriver->UpdateDDBFromBitmap(roomBackgroundBmp, thisroom.BgFrames[play.bg_frame].Graphic.get(), false);
}
if (gfxDriver->RequiresFullRedrawEachFrame())
{
if (current_background_is_dirty || walkBehindsCachedForBgNum != play.bg_frame)
{
if (walkBehindMethod == DrawAsSeparateSprite)
{
update_walk_behind_images();
}
}
add_thing_to_draw(roomBackgroundBmp, 0, 0, 0, false);
}
current_background_is_dirty = false; // Note this is only place where this flag is checked
clear_sprite_list();
if ((debug_flags & DBG_NOOBJECTS) == 0)
{
prepare_objects_for_drawing();
prepare_characters_for_drawing();
if ((debug_flags & DBG_NODRAWSPRITES) == 0)
{
our_eip = 34;
draw_sprite_list();
}
}
our_eip = 36;
}
// Draws the black surface behind (or rather between) the room viewports
void draw_preroom_background()
{
if (gfxDriver->RequiresFullRedrawEachFrame())
return;
update_black_invreg_and_reset(gfxDriver->GetMemoryBackBuffer());
}
// Draws the room background on the given surface.
//
// NOTE that this is **strictly** for software rendering.
// ds is a full game screen surface, and roomcam_surface is a surface for drawing room camera content to.
// ds and roomcam_surface may be the same bitmap.
// no_transform flag tells to copy dirty regions on roomcam_surface without any coordinate conversion
// whatsoever.
PBitmap draw_room_background(Viewport *view, const SpriteTransform &room_trans)
{
our_eip = 31;
// For the sake of software renderer, if there is any kind of camera transform required
// except screen offset, we tell it to draw on separate bitmap first with zero transformation.
// There are few reasons for this, primary is that Allegro does not support StretchBlt
// between different colour depths (i.e. it won't correctly stretch blit 16-bit rooms to
// 32-bit virtual screen).
// Also see comment to ALSoftwareGraphicsDriver::RenderToBackBuffer().
const int view_index = view->GetID();
Bitmap *ds = gfxDriver->GetMemoryBackBuffer();
// If separate bitmap was prepared for this view/camera pair then use it, draw untransformed
// and blit transformed whole surface later.
const bool draw_to_camsurf = CameraDrawData[view_index].Frame != nullptr;
Bitmap *roomcam_surface = draw_to_camsurf ? CameraDrawData[view_index].Frame.get() : ds;
{
// For software renderer: copy dirty rects onto the virtual screen.
// TODO: that would be SUPER NICE to reorganize the code and move this operation into SoftwareGraphicDriver somehow.
// Because basically we duplicate sprite batch transform here.
auto camera = view->GetCamera();
set_invalidrects_cameraoffs(view_index, camera->GetRect().Left, camera->GetRect().Top);
// TODO: (by CJ)
// the following line takes up to 50% of the game CPU time at
// high resolutions and colour depths - if we can optimise it
// somehow, significant performance gains to be had
update_room_invreg_and_reset(view_index, roomcam_surface, thisroom.BgFrames[play.bg_frame].Graphic.get(), draw_to_camsurf);
}
return CameraDrawData[view_index].Frame;
}
void draw_fps(const Rect &viewport)
{
// TODO: make allocated "fps struct" instead of using static vars!!
static IDriverDependantBitmap* ddb = nullptr;
static Bitmap *fpsDisplay = nullptr;
const int font = FONT_NORMAL;
if (fpsDisplay == nullptr)
{
fpsDisplay = BitmapHelper::CreateBitmap(viewport.GetWidth(), (getfontheight_outlined(font) + get_fixed_pixel_size(5)), game.GetColorDepth());
fpsDisplay = ReplaceBitmapWithSupportedFormat(fpsDisplay);
}
fpsDisplay->ClearTransparent();
color_t text_color = fpsDisplay->GetCompatibleColor(14);
char base_buffer[20];
if (!isTimerFpsMaxed()) {
sprintf(base_buffer, "%d", frames_per_second);
} else {
sprintf(base_buffer, "unlimited");
}
char fps_buffer[60];
// Don't display fps if we don't have enough information (because loop count was just reset)
if (!std::isnan(fps)) {
snprintf(fps_buffer, sizeof(fps_buffer), "FPS: %2.1f / %s", fps, base_buffer);
} else {
snprintf(fps_buffer, sizeof(fps_buffer), "FPS: --.- / %s", base_buffer);
}
wouttext_outline(fpsDisplay, 1, 1, font, text_color, fps_buffer);
char loop_buffer[60];
sprintf(loop_buffer, "Loop %u", loopcounter);
wouttext_outline(fpsDisplay, viewport.GetWidth() / 2, 1, font, text_color, loop_buffer);
if (ddb)
gfxDriver->UpdateDDBFromBitmap(ddb, fpsDisplay, false);
else
ddb = gfxDriver->CreateDDBFromBitmap(fpsDisplay, false);
int yp = viewport.GetHeight() - fpsDisplay->GetHeight();
gfxDriver->DrawSprite(1, yp, ddb);
invalidate_sprite(1, yp, ddb, false);
}
// Draw GUI and overlays of all kinds, anything outside the room space
void draw_gui_and_overlays()
{
if(pl_any_want_hook(AGSE_PREGUIDRAW))
add_thing_to_draw(nullptr, AGSE_PREGUIDRAW, 0, TRANS_RUN_PLUGIN, false);
// draw overlays, except text boxes and portraits
for (const auto &over : screenover) {
// complete overlay draw in non-transparent mode
if (over.type == OVER_COMPLETE)
add_thing_to_draw(over.bmp, over.x, over.y, TRANS_OPAQUE, false);
else if (over.type != OVER_TEXTMSG && over.type != OVER_PICTURE) {
int tdxp, tdyp;
get_overlay_position(over, &tdxp, &tdyp);
add_thing_to_draw(over.bmp, tdxp, tdyp, 0, over.hasAlphaChannel);
}
}
// Draw GUIs - they should always be on top of overlays like
// speech background text
our_eip=35;
if (((debug_flags & DBG_NOIFACE)==0) && (displayed_room >= 0)) {
int aa;
if (playerchar->activeinv >= MAX_INV) {
quit("!The player.activeinv variable has been corrupted, probably as a result\n"
"of an incorrect assignment in the game script.");
}
if (playerchar->activeinv < 1) gui_inv_pic=-1;
else gui_inv_pic=game.invinfo[playerchar->activeinv].pic;
our_eip = 37;
if (guis_need_update) {
guis_need_update = 0;
for (aa=0;aa<game.numgui;aa++) {
if (!guis[aa].IsDisplayed()) continue;
if (guibg[aa] == nullptr)
recreate_guibg_image(&guis[aa]);
eip_guinum = aa;
our_eip = 370;
guibg[aa]->ClearTransparent();
our_eip = 372;
guis[aa].DrawAt(guibg[aa], 0,0);
our_eip = 373;
bool isAlpha = false;
if (guis[aa].HasAlphaChannel())
{
isAlpha = true;
if ((game.options[OPT_NEWGUIALPHA] == kGuiAlphaRender_Legacy) && (guis[aa].BgImage > 0))
{
// old-style (pre-3.0.2) GUI alpha rendering
repair_alpha_channel(guibg[aa], spriteset[guis[aa].BgImage]);
}
}
if (guibgbmp[aa] != nullptr)
{
gfxDriver->UpdateDDBFromBitmap(guibgbmp[aa], guibg[aa], isAlpha);
}
else
{
guibgbmp[aa] = gfxDriver->CreateDDBFromBitmap(guibg[aa], isAlpha);
}
our_eip = 374;
}
}
our_eip = 38;
// Draw the GUIs
for (int gg = 0; gg < game.numgui; gg++) {
aa = play.gui_draw_order[gg];
if (!guis[aa].IsDisplayed()) continue;
// Don't draw GUI if "GUIs Turn Off When Disabled"
if ((game.options[OPT_DISABLEOFF] == 3) &&
(all_buttons_disabled > 0) &&
(guis[aa].PopupStyle != kGUIPopupNoAutoRemove))
continue;
add_thing_to_draw(guibgbmp[aa], guis[aa].X, guis[aa].Y, guis[aa].Transparency, guis[aa].HasAlphaChannel());
// only poll if the interface is enabled (mouseovers should not
// work while in Wait state)
if (IsInterfaceEnabled())
guis[aa].Poll();
}
}
// draw speech and portraits (so that they appear over GUIs)
for (const auto &over : screenover)
{
if (over.type == OVER_TEXTMSG || over.type == OVER_PICTURE)
{
int tdxp, tdyp;
get_overlay_position(over, &tdxp, &tdyp);
add_thing_to_draw(over.bmp, tdxp, tdyp, 0, false);
}
}
our_eip = 1099;
}
// Push the gathered list of sprites into the active graphic renderer
void put_sprite_list_on_screen(bool in_room)
{
// *** Draw the Things To Draw List ***
SpriteListEntry *thisThing;
for (size_t i = 0; i < thingsToDrawList.size(); ++i)
{
thisThing = &thingsToDrawList[i];
if (thisThing->bmp != nullptr) {
// mark the image's region as dirty
invalidate_sprite(thisThing->x, thisThing->y, thisThing->bmp, in_room);
}
else if ((thisThing->transparent != TRANS_RUN_PLUGIN) &&
(thisThing->bmp == nullptr))
{
quit("Null pointer added to draw list");
}
if (thisThing->bmp != nullptr)
{
if (thisThing->transparent <= 255)
{
thisThing->bmp->SetTransparency(thisThing->transparent);
}
gfxDriver->DrawSprite(thisThing->x, thisThing->y, thisThing->bmp);
}
else if (thisThing->transparent == TRANS_RUN_PLUGIN)
{
// meta entry to run the plugin hook
gfxDriver->DrawSprite(thisThing->x, thisThing->y, nullptr);
}
else
quit("Unknown entry in draw list");
}
our_eip = 1100;
}
bool GfxDriverNullSpriteCallback(int x, int y)
{
if (displayed_room < 0)
{
// if no room loaded, various stuff won't be initialized yet
return 1;
}
return (pl_run_plugin_hooks(x, y) != 0);
}
void GfxDriverOnInitCallback(void *data)
{
pl_run_plugin_init_gfx_hooks(gfxDriver->GetDriverID(), data);
}
// Schedule room rendering: background, objects, characters
static void construct_room_view()
{
draw_preroom_background();
prepare_room_sprites();
// reset the Baselines Changed flag now that we've drawn stuff
walk_behind_baselines_changed = 0;
for (const auto &viewport : play.GetRoomViewportsZOrdered())
{
if (!viewport->IsVisible())
continue;
auto camera = viewport->GetCamera();
if (!camera)
continue;
const Rect &view_rc = play.GetRoomViewportAbs(viewport->GetID());
const Rect &cam_rc = camera->GetRect();
SpriteTransform room_trans(-cam_rc.Left, -cam_rc.Top,
(float)view_rc.GetWidth() / (float)cam_rc.GetWidth(),
(float)view_rc.GetHeight() / (float)cam_rc.GetHeight(),
0.f);
if (gfxDriver->RequiresFullRedrawEachFrame())
{ // we draw everything as a sprite stack
gfxDriver->BeginSpriteBatch(view_rc, room_trans, Point(0, play.shake_screen_yoff), (GlobalFlipType)play.screen_flipped);
}
else
{
if (CameraDrawData[viewport->GetID()].Frame == nullptr && CameraDrawData[viewport->GetID()].IsOverlap)
{ // room background is prepended to the sprite stack
// TODO: here's why we have blit whole piece of background now:
// if we draw directly to the virtual screen overlapping another
// viewport, then we'd have to also mark and repaint every our
// region located directly over their dirty regions. That would
// require to update regions up the stack, converting their
// coordinates (cam1 -> screen -> cam2).
// It's not clear whether this is worth the effort, but if it is,
// then we'd need to optimise view/cam data first.
gfxDriver->BeginSpriteBatch(view_rc, room_trans);
gfxDriver->DrawSprite(0, 0, roomBackgroundBmp);
}
else
{ // room background is drawn by dirty rects system
PBitmap bg_surface = draw_room_background(viewport.get(), room_trans);
gfxDriver->BeginSpriteBatch(view_rc, room_trans, Point(), kFlip_None, bg_surface);
}
}
put_sprite_list_on_screen(true);
}
clear_draw_list();
}
// Schedule ui rendering
static void construct_ui_view()
{
gfxDriver->BeginSpriteBatch(play.GetUIViewportAbs(), SpriteTransform(), Point(0, play.shake_screen_yoff), (GlobalFlipType)play.screen_flipped);
draw_gui_and_overlays();
put_sprite_list_on_screen(false);
clear_draw_list();
}
void construct_game_scene(bool full_redraw)
{
gfxDriver->ClearDrawLists();
if (play.fast_forward)
return;
our_eip=3;
// React to changes to viewports and cameras (possibly from script) just before the render
play.UpdateViewports();
gfxDriver->UseSmoothScaling(IS_ANTIALIAS_SPRITES);
gfxDriver->RenderSpritesAtScreenResolution(usetup.RenderAtScreenRes, usetup.Supersampling);
pl_run_plugin_hooks(AGSE_PRERENDER, 0);
// Possible reasons to invalidate whole screen for the software renderer
if (full_redraw || play.screen_tint > 0 || play.shakesc_length > 0)
invalidate_screen();
// TODO: move to game update! don't call update during rendering pass!
// IMPORTANT: keep the order same because sometimes script may depend on it
if (displayed_room >= 0)
play.UpdateRoomCameras();
// Stage: room viewports
if (play.screen_is_faded_out == 0 && is_complete_overlay == 0)
{
if (displayed_room >= 0)
{
construct_room_view();
update_polled_mp3();
}
else if (!gfxDriver->RequiresFullRedrawEachFrame())
{
// black it out so we don't get cursor trails
// TODO: this is possible to do with dirty rects system now too (it can paint black rects outside of room viewport)
gfxDriver->GetMemoryBackBuffer()->Fill(0);
}
}
our_eip=4;
// Stage: UI overlay
if (play.screen_is_faded_out == 0)
{
construct_ui_view();
}
}
void construct_game_screen_overlay(bool draw_mouse)
{
gfxDriver->BeginSpriteBatch(play.GetMainViewport(), SpriteTransform(), Point(0, play.shake_screen_yoff), (GlobalFlipType)play.screen_flipped);
if (pl_any_want_hook(AGSE_POSTSCREENDRAW))
gfxDriver->DrawSprite(AGSE_POSTSCREENDRAW, 0, nullptr);
// TODO: find out if it's okay to move cursor animation and state update
// to the update loop instead of doing it in the drawing routine
// update animating mouse cursor
if (game.mcurs[cur_cursor].view >= 0) {
ags_domouse(DOMOUSE_NOCURSOR);
// only on mousemove, and it's not moving
if (((game.mcurs[cur_cursor].flags & MCF_ANIMMOVE) != 0) &&
(mousex == lastmx) && (mousey == lastmy));
// only on hotspot, and it's not on one
else if (((game.mcurs[cur_cursor].flags & MCF_HOTSPOT) != 0) &&
(GetLocationType(game_to_data_coord(mousex), game_to_data_coord(mousey)) == 0))
set_new_cursor_graphic(game.mcurs[cur_cursor].pic);
else if (mouse_delay>0) mouse_delay--;
else {
int viewnum = game.mcurs[cur_cursor].view;
int loopnum = 0;
if (loopnum >= views[viewnum].numLoops)
quitprintf("An animating mouse cursor is using view %d which has no loops", viewnum + 1);
if (views[viewnum].loops[loopnum].numFrames < 1)
quitprintf("An animating mouse cursor is using view %d which has no frames in loop %d", viewnum + 1, loopnum);
mouse_frame++;
if (mouse_frame >= views[viewnum].loops[loopnum].numFrames)
mouse_frame = 0;
set_new_cursor_graphic(views[viewnum].loops[loopnum].frames[mouse_frame].pic);
mouse_delay = views[viewnum].loops[loopnum].frames[mouse_frame].speed + 5;
CheckViewFrame(viewnum, loopnum, mouse_frame);
}
lastmx = mousex; lastmy = mousey;
}
ags_domouse(DOMOUSE_NOCURSOR);
// Stage: mouse cursor
if (draw_mouse && !play.mouse_cursor_hidden && play.screen_is_faded_out == 0)
{
gfxDriver->DrawSprite(mousex - hotx, mousey - hoty, mouseCursor);
invalidate_sprite(mousex - hotx, mousey - hoty, mouseCursor, false);
}
if (play.screen_is_faded_out == 0)
{
// Stage: screen fx
if (play.screen_tint >= 1)
gfxDriver->SetScreenTint(play.screen_tint & 0xff, (play.screen_tint >> 8) & 0xff, (play.screen_tint >> 16) & 0xff);
// Stage: legacy letterbox mode borders
render_black_borders();
}
if (play.screen_is_faded_out != 0 && gfxDriver->RequiresFullRedrawEachFrame())
{
const Rect &main_viewport = play.GetMainViewport();
gfxDriver->BeginSpriteBatch(main_viewport, SpriteTransform());
gfxDriver->SetScreenFade(play.fade_to_red, play.fade_to_green, play.fade_to_blue);
}
}
void construct_engine_overlay()
{
const Rect &viewport = RectWH(game.GetGameRes());
gfxDriver->BeginSpriteBatch(viewport, SpriteTransform());
// draw the debug console, if appropriate
if ((play.debug_mode > 0) && (display_console != 0))
{
const int font = FONT_NORMAL;
int ypp = 1;
int txtspacing = getfontspacing_outlined(font);
int barheight = getheightoflines(font, DEBUG_CONSOLE_NUMLINES - 1) + 4;
if (debugConsoleBuffer == nullptr)
{
debugConsoleBuffer = BitmapHelper::CreateBitmap(viewport.GetWidth(), barheight, game.GetColorDepth());
debugConsoleBuffer = ReplaceBitmapWithSupportedFormat(debugConsoleBuffer);
}
color_t draw_color = debugConsoleBuffer->GetCompatibleColor(15);
debugConsoleBuffer->FillRect(Rect(0, 0, viewport.GetWidth() - 1, barheight), draw_color);
color_t text_color = debugConsoleBuffer->GetCompatibleColor(16);
for (int jj = first_debug_line; jj != last_debug_line; jj = (jj + 1) % DEBUG_CONSOLE_NUMLINES) {
wouttextxy(debugConsoleBuffer, 1, ypp, font, text_color, debug_line[jj]);
ypp += txtspacing;
}
if (debugConsole == nullptr)
debugConsole = gfxDriver->CreateDDBFromBitmap(debugConsoleBuffer, false, true);
else
gfxDriver->UpdateDDBFromBitmap(debugConsole, debugConsoleBuffer, false);
gfxDriver->DrawSprite(0, 0, debugConsole);
invalidate_sprite(0, 0, debugConsole, false);
}
if (display_fps != kFPS_Hide)
draw_fps(viewport);
}
static void update_shakescreen()
{
// TODO: unify blocking and non-blocking shake update
play.shake_screen_yoff = 0;
if (play.shakesc_length > 0)
{
if ((loopcounter % play.shakesc_delay) < (play.shakesc_delay / 2))
play.shake_screen_yoff = play.shakesc_amount;
}
}
// Draw everything
void render_graphics(IDriverDependantBitmap *extraBitmap, int extraX, int extraY)
{
// Don't render if skipping cutscene
if (play.fast_forward)
return;
// Don't render if we've just entered new room and are before fade-in
// TODO: find out why this is not skipped for 8-bit games
if ((in_new_room > 0) & (game.color_depth > 1))
return;
// TODO: find out if it's okay to move shake to update function
update_shakescreen();
construct_game_scene(false);
our_eip=5;
// NOTE: extraBitmap will always be drawn with the UI render stage
if (extraBitmap != nullptr)
{
invalidate_sprite(extraX, extraY, extraBitmap, false);
gfxDriver->DrawSprite(extraX, extraY, extraBitmap);
}
construct_game_screen_overlay(true);
render_to_screen();
if (!play.screen_is_faded_out) {
// always update the palette, regardless of whether the plugin
// vetos the screen update
if (bg_just_changed) {
setpal();
bg_just_changed = 0;
}
}
screen_is_dirty = false;
}