/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "ultima/ultima4/gfx/screen.h" #include "common/system.h" #include "engines/util.h" #include "graphics/cursorman.h" #include "ultima/ultima4/controllers/intro_controller.h" #include "ultima/ultima4/core/config.h" #include "ultima/ultima4/core/settings.h" #include "ultima/ultima4/core/utils.h" #include "ultima/ultima4/events/event_handler.h" #include "ultima/ultima4/filesys/savegame.h" #include "ultima/ultima4/game/context.h" #include "ultima/ultima4/game/names.h" #include "ultima/ultima4/game/object.h" #include "ultima/ultima4/game/player.h" #include "ultima/ultima4/gfx/imagemgr.h" #include "ultima/ultima4/gfx/textcolor.h" #include "ultima/ultima4/map/annotation.h" #include "ultima/ultima4/map/location.h" #include "ultima/ultima4/map/tileanim.h" #include "ultima/ultima4/map/tileset.h" #include "ultima/ultima4/ultima4.h" #include "ultima/ultima4/views/dungeonview.h" #include "ultima/ultima4/views/tileview.h" namespace Ultima { namespace Ultima4 { #define MOUSE_CURSOR_SIZE 20 #ifndef DBL_MAX #define DBL_MAX 1e99 #endif #define BUFFER_SIZE 1024 Screen *g_screen; Screen::Screen() : _filterScaler(nullptr), _currentMouseCursor(-1), _gemLayout(nullptr), _tileAnims(nullptr), _charSetInfo(nullptr), _gemTilesInfo(nullptr), _needPrompt(1), _currentCycle(0), _cursorStatus(0), _cursorEnabled(1), _priorFrameTime(0) /* , _continueScreenRefresh(true) */ { g_screen = this; Common::fill(&_mouseCursors[0], &_mouseCursors[5], (MouseCursorSurface *)nullptr); Common::fill(&_los[0][0], &_los[0][0] + (VIEWPORT_W * VIEWPORT_H), 0); _filterNames.clear(); _filterNames.push_back("point"); _filterNames.push_back("2xBi"); _filterNames.push_back("2xSaI"); _filterNames.push_back("Scale2x"); _lineOfSightStyles.clear(); _lineOfSightStyles.push_back("DOS"); _lineOfSightStyles.push_back("Enhanced"); } Screen::~Screen() { clear(); for (uint idx = 0; idx < _tileAnimSets.size(); ++idx) delete _tileAnimSets[idx]; g_screen = nullptr; } void Screen::init() { Common::Point size(SCREEN_WIDTH * settings._scale, SCREEN_HEIGHT * settings._scale); initGraphics(size.x, size.y, nullptr); create(size.x, size.y, g_system->getScreenFormat()); loadMouseCursors(); screenLoadGraphicsFromConf(); debug(1, "using %s scaler\n", settings._filter.c_str()); /* find the tile animations for our tileset */ _tileAnims = nullptr; for (auto *set : _tileAnimSets) { if (set->_name == settings._videoType) _tileAnims = set; } if (!_tileAnims) error("unable to find tile animations for \"%s\" video mode in graphics.xml", settings._videoType.c_str()); _dungeonTileChars.clear(); _dungeonTileChars["brick_floor"] = CHARSET_FLOOR; _dungeonTileChars["up_ladder"] = CHARSET_LADDER_UP; _dungeonTileChars["down_ladder"] = CHARSET_LADDER_DOWN; _dungeonTileChars["up_down_ladder"] = CHARSET_LADDER_UPDOWN; _dungeonTileChars["chest"] = '$'; _dungeonTileChars["ceiling_hole"] = CHARSET_FLOOR; _dungeonTileChars["floor_hole"] = CHARSET_FLOOR; _dungeonTileChars["magic_orb"] = CHARSET_ORB; _dungeonTileChars["ceiling_hole"] = 'T'; _dungeonTileChars["floor_hole"] = 'T'; _dungeonTileChars["fountain"] = 'F'; _dungeonTileChars["secret_door"] = CHARSET_SDOOR; _dungeonTileChars["brick_wall"] = CHARSET_WALL; _dungeonTileChars["dungeon_door"] = CHARSET_ROOM; _dungeonTileChars["avatar"] = CHARSET_REDDOT; _dungeonTileChars["dungeon_room"] = CHARSET_ROOM; _dungeonTileChars["dungeon_altar"] = CHARSET_ANKH; _dungeonTileChars["energy_field"] = '^'; _dungeonTileChars["fire_field"] = '^'; _dungeonTileChars["poison_field"] = '^'; _dungeonTileChars["sleep_field"] = '^'; } void Screen::clear() { // Clear any pending updates for the current screen update(); for (auto *layout : _layouts) delete layout; _layouts.clear(); ImageMgr::destroy(); _charSetInfo = nullptr; // Delete cursors for (int idx = 0; idx < 5; ++idx) { delete _mouseCursors[idx]; _mouseCursors[idx] = nullptr; } } void Screen::loadMouseCursors() { // enable or disable the mouse cursor if (settings._mouseOptions._enabled) { Common::File cursorsFile; if (!cursorsFile.open("data/graphics/cursors.txt")) error("Could not load mouse cursors"); for (int idx = 0; idx < 5; ++idx) _mouseCursors[idx] = loadMouseCursor(cursorsFile); // Set the default initial cursor const uint TRANSPARENT = format.RGBToColor(0x80, 0x80, 0x80); MouseCursorSurface *c = _mouseCursors[MC_DEFAULT]; CursorMan.pushCursor(*c, c->_hotspot.x, c->_hotspot.y, TRANSPARENT, false); CursorMan.showMouse(true); } else { CursorMan.showMouse(false); } _filterScaler = scalerGet(settings._filter); if (!_filterScaler) error("%s is not a valid filter", settings._filter.c_str()); } void Screen::setMouseCursor(MouseCursor cursor) { const MouseCursorSurface *c = _mouseCursors[cursor]; if (c && cursor != _currentMouseCursor) { _currentMouseCursor = cursor; const uint TRANSPARENT = format.RGBToColor(0x80, 0x80, 0x80); CursorMan.replaceCursor(*c, c->_hotspot.x, c->_hotspot.y, TRANSPARENT, false); } } MouseCursorSurface *Screen::loadMouseCursor(Common::File &src) { uint row, col, endCol, pixel; int hotX, hotY; Common::String line; byte *destP; const uint WHITE = format.RGBToColor(0xff, 0xff, 0xff); const uint BLACK = format.RGBToColor(0, 0, 0); const uint TRANSPARENT = format.RGBToColor(0x80, 0x80, 0x80); int bpp = format.bytesPerPixel; assert(bpp >= 2); MouseCursorSurface *c = new MouseCursorSurface(); c->create(MOUSE_CURSOR_SIZE, MOUSE_CURSOR_SIZE, format); c->clear(TRANSPARENT); for (row = 0; row < MOUSE_CURSOR_SIZE; row++) { line = src.readLine(); destP = (byte *)c->getBasePtr(0, row); endCol = MIN(line.size(), (uint)MOUSE_CURSOR_SIZE); for (col = 0; col < endCol; ++col, destP += bpp) { pixel = TRANSPARENT; if (line[col] == 'X') pixel = BLACK; else if (line[col] == '.') pixel = WHITE; if (bpp == 2) *((uint16 *)destP) = pixel; else *((uint32 *)destP) = pixel; } } // Read in the hotspot position line = src.readLine(); (void)sscanf(line.c_str(), "%d,%d", &hotX, &hotY); c->_hotspot.x = hotX; c->_hotspot.y = hotY; return c; } void Screen::screenReInit() { g_intro->deleteIntro(); // delete intro stuff g_tileSets->unloadAllImages(); // unload tilesets, which will be reloaded lazily as needed ImageMgr::destroy(); _tileAnims = nullptr; clear(); init(); // Re-init screen stuff (loading new backgrounds, etc.) g_intro->init(); // Re-fix the backgrounds loaded and scale images, etc. } void Screen::screenTextAt(int x, int y, const char *fmt, ...) { char buffer[BUFFER_SIZE]; uint i; va_list args; va_start(args, fmt); vsnprintf(buffer, BUFFER_SIZE, fmt, args); va_end(args); for (i = 0; i < strlen(buffer); i++) screenShowChar(buffer[i], x + i, y); } void Screen::screenPrompt() { if (_needPrompt && _cursorEnabled && g_context->_col == 0) { screenMessage("%c", CHARSET_PROMPT); _needPrompt = 0; } } void Screen::screenMessage(const char *fmt, ...) { #ifdef IOS_ULTIMA4 static bool recursed = false; #endif if (!g_context) return; //Because some cases (like the intro) don't have the context initiated. char buffer[BUFFER_SIZE]; uint i; int wordlen; va_list args; va_start(args, fmt); vsnprintf(buffer, BUFFER_SIZE, fmt, args); va_end(args); #ifdef IOS_ULTIMA4 if (recursed) recursed = false; else U4IOS::drawMessageOnLabel(Common::String(buffer, 1024)); #endif screenHideCursor(); /* scroll the message area, if necessary */ if (g_context->_line == 12) { screenScrollMessageArea(); g_context->_line--; } for (i = 0; i < strlen(buffer); i++) { // include whitespace and color-change codes wordlen = strcspn(buffer + i, " \b\t\n\024\025\026\027\030\031"); /* backspace */ if (buffer[i] == '\b') { g_context->_col--; if (g_context->_col < 0) { g_context->_col += 16; g_context->_line--; } continue; } /* color-change codes */ switch (buffer[i]) { case FG_GREY: case FG_BLUE: case FG_PURPLE: case FG_GREEN: case FG_RED: case FG_YELLOW: case FG_WHITE: screenTextColor(buffer[i]); continue; } /* check for word wrap */ if ((g_context->_col + wordlen > 16) || buffer[i] == '\n' || g_context->_col == 16) { if (buffer[i] == '\n' || buffer[i] == ' ') i++; g_context->_line++; g_context->_col = 0; #ifdef IOS_ULTIMA4 recursed = true; #endif screenMessage("%s", buffer + i); return; } /* code for move cursor right */ if (buffer[i] == 0x12) { g_context->_col++; continue; } /* don't show a space in column 1. Helps with Hawkwind. */ if (buffer[i] == ' ' && g_context->_col == 0) continue; screenShowChar(buffer[i], TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line); g_context->_col++; } screenSetCursorPos(TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line); screenShowCursor(); _needPrompt = 1; } void Screen::screenLoadGraphicsFromConf() { const Config *config = Config::getInstance(); Std::vector graphicsConf = config->getElement("graphics").getChildren(); for (const auto &conf : graphicsConf) { if (conf.getName() == "layout") _layouts.push_back(screenLoadLayoutFromConf(conf)); else if (conf.getName() == "tileanimset") _tileAnimSets.push_back(new TileAnimSet(conf)); } _gemLayoutNames.clear(); for (const auto *layout : _layouts) { if (layout->_type == LAYOUT_GEM) { _gemLayoutNames.push_back(layout->_name); } } /* * Find gem layout to use. */ for (auto *layout : _layouts) { if (layout->_type == LAYOUT_GEM && layout->_name == settings._gemLayout) { _gemLayout = layout; break; } } if (!_gemLayout) error("no gem layout named %s found!\n", settings._gemLayout.c_str()); } Layout *Screen::screenLoadLayoutFromConf(const ConfigElement &conf) { Layout *layout; static const char *const typeEnumStrings[] = {"standard", "gem", "dungeon_gem", nullptr}; layout = new Layout(); layout->_name = conf.getString("name"); layout->_type = static_cast(conf.getEnum("type", typeEnumStrings)); Std::vector children = conf.getChildren(); for (const auto &i : children) { if (i.getName() == "tileshape") { layout->_tileShape.x = i.getInt("width"); layout->_tileShape.y = i.getInt("height"); } else if (i.getName() == "viewport") { layout->_viewport.left = i.getInt("x"); layout->_viewport.top = i.getInt("y"); layout->_viewport.setWidth(i.getInt("width")); layout->_viewport.setHeight(i.getInt("height")); } } return layout; } Std::vector Screen::screenViewportTile(uint width, uint height, int x, int y, bool &focus) { MapCoords center = g_context->_location->_coords; static MapTile grass = g_context->_location->_map->_tileSet->getByName("grass")->getId(); if (g_context->_location->_map->_width <= width && g_context->_location->_map->_height <= height) { center.x = g_context->_location->_map->_width / 2; center.y = g_context->_location->_map->_height / 2; } MapCoords tc = center; tc.x += x - (width / 2); tc.y += y - (height / 2); /* Wrap the location if we can */ tc.wrap(g_context->_location->_map); /* off the edge of the map: pad with grass tiles */ if (MAP_IS_OOB(g_context->_location->_map, tc)) { focus = false; Std::vector result; result.push_back(grass); return result; } return g_context->_location->tilesAt(tc, focus); } bool Screen::screenTileUpdate(TileView *view, const Coords &coords, bool redraw) { if (g_context->_location->_map->_flags & FIRST_PERSON) return false; // Get the tiles bool focus; MapCoords mc(coords); mc.wrap(g_context->_location->_map); Std::vector tiles = g_context->_location->tilesAt(mc, focus); // Get the screen coordinates int x = coords.x; int y = coords.y; if (g_context->_location->_map->_width > VIEWPORT_W || g_context->_location->_map->_height > VIEWPORT_H) { //Center the coordinates to the viewport if you're on centered-view map. x = x - g_context->_location->_coords.x + VIEWPORT_W / 2; y = y - g_context->_location->_coords.y + VIEWPORT_H / 2; } // Draw if it is on screen if (x >= 0 && y >= 0 && x < VIEWPORT_W && y < VIEWPORT_H && _los[x][y]) { view->drawTile(tiles, focus, x, y); if (redraw) { //screenRedrawMapArea(); } return true; } return false; } void Screen::screenUpdate(TileView *view, bool showmap, bool blackout) { assertMsg(g_context != nullptr, "context has not yet been initialized"); if (blackout) { screenEraseMapArea(); } else if (g_context->_location->_viewMode == VIEW_GEM) { // No need to render view when cheat overhead map showing } else if (g_context->_location->_map->_flags & FIRST_PERSON) { DungeonViewer.display(g_context, view); screenRedrawMapArea(); } else if (showmap) { static MapTile black = g_context->_location->_map->_tileSet->getByName("black")->getId(); //static MapTile avatar = g_context->_location->_map->_tileset->getByName("avatar")->getId(); int x, y; Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]; bool viewportFocus[VIEWPORT_W][VIEWPORT_H]; for (y = 0; y < VIEWPORT_H; y++) { for (x = 0; x < VIEWPORT_W; x++) { viewportTiles[x][y] = screenViewportTile(VIEWPORT_W, VIEWPORT_H, x, y, viewportFocus[x][y]); } } screenFindLineOfSight(viewportTiles); for (y = 0; y < VIEWPORT_H; y++) { for (x = 0; x < VIEWPORT_W; x++) { if (_los[x][y]) { view->drawTile(viewportTiles[x][y], viewportFocus[x][y], x, y); } else view->drawTile(black, false, x, y); } } screenRedrawMapArea(); } screenUpdateCursor(); screenUpdateMoons(); screenUpdateWind(); } void Screen::screenDrawImage(const Common::String &name, int x, int y) { ImageInfo *info = imageMgr->get(name); if (info) { info->_image->alphaOn(); info->_image->draw(x, y); return; } SubImage *subimage = imageMgr->getSubImage(name); if (subimage) info = imageMgr->get(subimage->_srcImageName); if (info) { info->_image->alphaOn(); if (info) { info->_image->drawSubRect(x, y, subimage->left * (settings._scale / info->_prescale), subimage->top * (settings._scale / info->_prescale), subimage->width() * (settings._scale / info->_prescale), subimage->height() * (settings._scale / info->_prescale)); return; } } error("ERROR 1006: Unable to load the image \"%s\"", name.c_str()); } void Screen::screenDrawImageInMapArea(const Common::String &name) { ImageInfo *info; info = imageMgr->get(name); if (!info) error("ERROR 1004: Unable to load data files"); info->_image->drawSubRect(BORDER_WIDTH * settings._scale, BORDER_HEIGHT * settings._scale, BORDER_WIDTH * settings._scale, BORDER_HEIGHT * settings._scale, VIEWPORT_W * TILE_WIDTH * settings._scale, VIEWPORT_H * TILE_HEIGHT * settings._scale); } void Screen::screenTextColor(int color) { if (_charSetInfo == nullptr) { _charSetInfo = imageMgr->get(BKGD_CHARSET); if (!_charSetInfo) error("ERROR 1003: Unable to load the \"%s\" data file", BKGD_CHARSET); } if (!settings._enhancements || !settings._enhancementsOptions._textColorization) { return; } switch (color) { case FG_GREY: case FG_BLUE: case FG_PURPLE: case FG_GREEN: case FG_RED: case FG_YELLOW: case FG_WHITE: _charSetInfo->_image->setFontColorFG((ColorFG)color); } } void Screen::screenShowChar(int chr, int x, int y) { if (_charSetInfo == nullptr) { _charSetInfo = imageMgr->get(BKGD_CHARSET); if (!_charSetInfo) error("ERROR 1001: Unable to load the \"%s\" data file", BKGD_CHARSET); } _charSetInfo->_image->drawSubRect(x * _charSetInfo->_image->width(), y * (CHAR_HEIGHT * settings._scale), 0, chr * (CHAR_HEIGHT * settings._scale), _charSetInfo->_image->width(), CHAR_HEIGHT * settings._scale); } void Screen::screenScrollMessageArea() { assertMsg(_charSetInfo != nullptr && _charSetInfo->_image != nullptr, "charset not initialized!"); Image *screen = imageMgr->get("screen")->_image; screen->drawSubRectOn(screen, TEXT_AREA_X * _charSetInfo->_image->width(), TEXT_AREA_Y * CHAR_HEIGHT * settings._scale, TEXT_AREA_X * _charSetInfo->_image->width(), (TEXT_AREA_Y + 1) * CHAR_HEIGHT * settings._scale, TEXT_AREA_W * _charSetInfo->_image->width(), (TEXT_AREA_H - 1) * CHAR_HEIGHT * settings._scale); screen->fillRect(TEXT_AREA_X * _charSetInfo->_image->width(), TEXT_AREA_Y * CHAR_HEIGHT * settings._scale + (TEXT_AREA_H - 1) * CHAR_HEIGHT * settings._scale, TEXT_AREA_W * _charSetInfo->_image->width(), CHAR_HEIGHT * settings._scale, 0, 0, 0); update(); } void Screen::screenFrame() { uint32 time = g_system->getMillis(); if (time >= (_priorFrameTime + SCREEN_FRAME_TIME)) { _priorFrameTime = time; update(); } } void Screen::screenCycle() { if (++_currentCycle >= SCR_CYCLE_MAX) _currentCycle = 0; } void Screen::screenUpdateCursor() { int phase = _currentCycle * SCR_CYCLE_PER_SECOND / SCR_CYCLE_MAX; assertMsg(phase >= 0 && phase < 4, "derived an invalid cursor phase: %d", phase); if (_cursorStatus) { screenShowChar(31 - phase, _cursorPos.x, _cursorPos.y); screenRedrawTextArea(_cursorPos.x, _cursorPos.y, 1, 1); } } void Screen::screenUpdateMoons() { int trammelChar, feluccaChar; /* show "L?" for the dungeon level */ if (g_context->_location->_context == CTX_DUNGEON) { screenShowChar('L', 11, 0); screenShowChar('1' + g_context->_location->_coords.z, 12, 0); } /* show the current moons (non-combat) */ else if ((g_context->_location->_context & CTX_NON_COMBAT) == g_context->_location->_context) { trammelChar = (g_ultima->_saveGame->_trammelPhase == 0) ? MOON_CHAR + 7 : MOON_CHAR + g_ultima->_saveGame->_trammelPhase - 1; feluccaChar = (g_ultima->_saveGame->_feluccaPhase == 0) ? MOON_CHAR + 7 : MOON_CHAR + g_ultima->_saveGame->_feluccaPhase - 1; screenShowChar(trammelChar, 11, 0); screenShowChar(feluccaChar, 12, 0); } screenRedrawTextArea(11, 0, 2, 1); } void Screen::screenUpdateWind() { /* show the direction we're facing in the dungeon */ if (g_context->_location->_context == CTX_DUNGEON) { screenEraseTextArea(WIND_AREA_X, WIND_AREA_Y, WIND_AREA_W, WIND_AREA_H); screenTextAt(WIND_AREA_X, WIND_AREA_Y, "Dir: %5s", getDirectionName((Direction)g_ultima->_saveGame->_orientation)); } /* show the wind direction */ else if ((g_context->_location->_context & CTX_NON_COMBAT) == g_context->_location->_context) { screenEraseTextArea(WIND_AREA_X, WIND_AREA_Y, WIND_AREA_W, WIND_AREA_H); screenTextAt(WIND_AREA_X, WIND_AREA_Y, "Wind %5s", getDirectionName((Direction)g_context->_windDirection)); } screenRedrawTextArea(WIND_AREA_X, WIND_AREA_Y, WIND_AREA_W, WIND_AREA_H); } void Screen::screenShowCursor() { if (!_cursorStatus && _cursorEnabled) { _cursorStatus = 1; screenUpdateCursor(); } } void Screen::screenHideCursor() { if (_cursorStatus) { screenEraseTextArea(_cursorPos.x, _cursorPos.y, 1, 1); screenRedrawTextArea(_cursorPos.x, _cursorPos.y, 1, 1); } _cursorStatus = 0; } void Screen::screenEnableCursor(void) { _cursorEnabled = 1; } void Screen::screenDisableCursor(void) { screenHideCursor(); _cursorEnabled = 0; } void Screen::screenSetCursorPos(int x, int y) { _cursorPos.x = x; _cursorPos.y = y; } void Screen::screenFindLineOfSight(Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]) { int x, y; if (!g_context) return; /* * if the map has the no line of sight flag, all is visible */ if (g_context->_location->_map->_flags & NO_LINE_OF_SIGHT) { for (y = 0; y < VIEWPORT_H; y++) { for (x = 0; x < VIEWPORT_W; x++) { _los[x][y] = 1; } } return; } /* * otherwise calculate it from the map data */ for (y = 0; y < VIEWPORT_H; y++) { for (x = 0; x < VIEWPORT_W; x++) { _los[x][y] = 0; } } if (settings._lineOfSight == "DOS") screenFindLineOfSightDOS(viewportTiles); else if (settings._lineOfSight == "Enhanced") screenFindLineOfSightEnhanced(viewportTiles); else error("unknown line of sight style %s!\n", settings._lineOfSight.c_str()); } void Screen::screenFindLineOfSightDOS(Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]) { int x, y; _los[VIEWPORT_W / 2][VIEWPORT_H / 2] = 1; for (x = VIEWPORT_W / 2 - 1; x >= 0; x--) if (_los[x + 1][VIEWPORT_H / 2] && !viewportTiles[x + 1][VIEWPORT_H / 2].front().getTileType()->isOpaque()) _los[x][VIEWPORT_H / 2] = 1; for (x = VIEWPORT_W / 2 + 1; x < VIEWPORT_W; x++) if (_los[x - 1][VIEWPORT_H / 2] && !viewportTiles[x - 1][VIEWPORT_H / 2].front().getTileType()->isOpaque()) _los[x][VIEWPORT_H / 2] = 1; for (y = VIEWPORT_H / 2 - 1; y >= 0; y--) if (_los[VIEWPORT_W / 2][y + 1] && !viewportTiles[VIEWPORT_W / 2][y + 1].front().getTileType()->isOpaque()) _los[VIEWPORT_W / 2][y] = 1; for (y = VIEWPORT_H / 2 + 1; y < VIEWPORT_H; y++) if (_los[VIEWPORT_W / 2][y - 1] && !viewportTiles[VIEWPORT_W / 2][y - 1].front().getTileType()->isOpaque()) _los[VIEWPORT_W / 2][y] = 1; for (y = VIEWPORT_H / 2 - 1; y >= 0; y--) { for (x = VIEWPORT_W / 2 - 1; x >= 0; x--) { if (_los[x][y + 1] && !viewportTiles[x][y + 1].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x + 1][y] && !viewportTiles[x + 1][y].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x + 1][y + 1] && !viewportTiles[x + 1][y + 1].front().getTileType()->isOpaque()) _los[x][y] = 1; } for (x = VIEWPORT_W / 2 + 1; x < VIEWPORT_W; x++) { if (_los[x][y + 1] && !viewportTiles[x][y + 1].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x - 1][y] && !viewportTiles[x - 1][y].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x - 1][y + 1] && !viewportTiles[x - 1][y + 1].front().getTileType()->isOpaque()) _los[x][y] = 1; } } for (y = VIEWPORT_H / 2 + 1; y < VIEWPORT_H; y++) { for (x = VIEWPORT_W / 2 - 1; x >= 0; x--) { if (_los[x][y - 1] && !viewportTiles[x][y - 1].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x + 1][y] && !viewportTiles[x + 1][y].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x + 1][y - 1] && !viewportTiles[x + 1][y - 1].front().getTileType()->isOpaque()) _los[x][y] = 1; } for (x = VIEWPORT_W / 2 + 1; x < VIEWPORT_W; x++) { if (_los[x][y - 1] && !viewportTiles[x][y - 1].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x - 1][y] && !viewportTiles[x - 1][y].front().getTileType()->isOpaque()) _los[x][y] = 1; else if (_los[x - 1][y - 1] && !viewportTiles[x - 1][y - 1].front().getTileType()->isOpaque()) _los[x][y] = 1; } } } void Screen::screenFindLineOfSightEnhanced(Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]) { int x, y; /* * the shadow rasters for each viewport octant * * shadowRaster[0][0] // number of raster segments in this shadow * shadowRaster[0][1] // #1 shadow bitmask value (low three bits) + "newline" flag (high bit) * shadowRaster[0][2] // #1 length * shadowRaster[0][3] // #2 shadow bitmask value * shadowRaster[0][4] // #2 length * shadowRaster[0][5] // #3 shadow bitmask value * shadowRaster[0][6] // #3 length * ...etc... */ const int shadowRaster[14][13] = { {6, __VCH, 4, _N_CH, 1, __VCH, 3, _N___, 1, ___CH, 1, __VCH, 1}, // raster_1_0 {6, __VC_, 1, _NVCH, 2, __VC_, 1, _NVCH, 3, _NVCH, 2, _NVCH, 1}, // raster_1_1 // {4, __VCH, 3, _N__H, 1, ___CH, 1, __VCH, 1, 0, 0, 0, 0}, // raster_2_0 {6, __VC_, 2, _N_CH, 1, __VCH, 2, _N_CH, 1, __VCH, 1, _N__H, 1}, // raster_2_1 {6, __V__, 1, _NVCH, 1, __VC_, 1, _NVCH, 1, __VC_, 1, _NVCH, 1}, // raster_2_2 // {2, __VCH, 2, _N__H, 2, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_3_0 {3, __VC_, 2, _N_CH, 1, __VCH, 1, 0, 0, 0, 0, 0, 0}, // raster_3_1 {3, __VC_, 1, _NVCH, 2, _N_CH, 1, 0, 0, 0, 0, 0, 0}, // raster_3_2 {3, _NVCH, 1, __V__, 1, _NVCH, 1, 0, 0, 0, 0, 0, 0}, // raster_3_3 // {2, __VCH, 1, _N__H, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_0 {2, __VC_, 1, _N__H, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_1 {2, __VC_, 1, _N_CH, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_2 {2, __V__, 1, _NVCH, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_3 {2, __V__, 1, _NVCH, 1, 0, 0, 0, 0, 0, 0, 0, 0} // raster_4_4 }; /* * As each viewport tile is processed, it will store the bitmask for the shadow it casts. * Later, after processing all octants, the entire viewport will be marked visible except * for those tiles that have the __VCH bitmask. */ const int _OCTANTS = 8; const int _NUM_RASTERS_COLS = 4; int octant; int xOrigin, yOrigin, xSign = 0, ySign = 0, reflect = false, xTile, yTile, xTileOffset, yTileOffset; for (octant = 0; octant < _OCTANTS; octant++) { switch (octant) { case 0: xSign = 1; ySign = 1; reflect = false; break; // lower-right case 1: xSign = 1; ySign = 1; reflect = true; break; case 2: xSign = 1; ySign = -1; reflect = true; break; // lower-left case 3: xSign = -1; ySign = 1; reflect = false; break; case 4: xSign = -1; ySign = -1; reflect = false; break; // upper-left case 5: xSign = -1; ySign = -1; reflect = true; break; case 6: xSign = -1; ySign = 1; reflect = true; break; // upper-right case 7: xSign = 1; ySign = -1; reflect = false; break; } // determine the origin point for the current LOS octant xOrigin = VIEWPORT_W / 2; yOrigin = VIEWPORT_H / 2; // make sure the segment doesn't reach out of bounds int maxWidth = xOrigin; int maxHeight = yOrigin; int currentRaster = 0; // just in case the viewport isn't square, swap the width and height if (reflect) { // swap height and width for later use maxWidth ^= maxHeight; maxHeight ^= maxWidth; maxWidth ^= maxHeight; } // check the visibility of each tile for (int currentCol = 1; currentCol <= _NUM_RASTERS_COLS; currentCol++) { for (int currentRow = 0; currentRow <= currentCol; currentRow++) { // swap X and Y to reflect the octant rasters if (reflect) { xTile = xOrigin + (currentRow * ySign); yTile = yOrigin + (currentCol * xSign); } else { xTile = xOrigin + (currentCol * xSign); yTile = yOrigin + (currentRow * ySign); } if (viewportTiles[xTile][yTile].front().getTileType()->isOpaque()) { // a wall was detected, so go through the raster for this wall // segment and mark everything behind it with the appropriate // shadow bitmask. // // first, get the correct raster // if ((currentCol == 1) && (currentRow == 0)) { currentRaster = 0; } else if ((currentCol == 1) && (currentRow == 1)) { currentRaster = 1; } else if ((currentCol == 2) && (currentRow == 0)) { currentRaster = 2; } else if ((currentCol == 2) && (currentRow == 1)) { currentRaster = 3; } else if ((currentCol == 2) && (currentRow == 2)) { currentRaster = 4; } else if ((currentCol == 3) && (currentRow == 0)) { currentRaster = 5; } else if ((currentCol == 3) && (currentRow == 1)) { currentRaster = 6; } else if ((currentCol == 3) && (currentRow == 2)) { currentRaster = 7; } else if ((currentCol == 3) && (currentRow == 3)) { currentRaster = 8; } else if ((currentCol == 4) && (currentRow == 0)) { currentRaster = 9; } else if ((currentCol == 4) && (currentRow == 1)) { currentRaster = 10; } else if ((currentCol == 4) && (currentRow == 2)) { currentRaster = 11; } else if ((currentCol == 4) && (currentRow == 3)) { currentRaster = 12; } else { currentRaster = 13; // currentCol and currentRow must equal 4 } xTileOffset = 0; yTileOffset = 0; //======================================== for (int currentSegment = 0; currentSegment < shadowRaster[currentRaster][0]; currentSegment++) { // each shadow segment is 2 bytes int shadowType = shadowRaster[currentRaster][currentSegment * 2 + 1]; int shadowLength = shadowRaster[currentRaster][currentSegment * 2 + 2]; // update the raster length to make sure it fits in the viewport shadowLength = (shadowLength + 1 + yTileOffset > maxWidth ? maxWidth : shadowLength); // check to see if we should move up a row if (shadowType & 0x80) { // remove the flag from the shadowType shadowType ^= _N___; // if (currentRow + yTileOffset >= maxHeight) { if (currentRow + yTileOffset > maxHeight) { break; } xTileOffset = yTileOffset; yTileOffset++; } /* it is seemingly unnecessary to swap the edges for * shadow tiles, because we only care about shadow * tiles that have all three parts (V, C, and H) * flagged. if a tile has fewer than three, it is * ignored during the draw phase, so vertical and * horizontal shadow edge accuracy isn't important */ // if reflecting the octant, swap the edges // if (reflect) { // int shadowTemp = 0; // // swap the vertical and horizontal shadow edges // if (shadowType & __V__) { shadowTemp |= ____H; } // if (shadowType & ___C_) { shadowTemp |= ___C_; } // if (shadowType & ____H) { shadowTemp |= __V__; } // shadowType = shadowTemp; // } for (int currentShadow = 1; currentShadow <= shadowLength; currentShadow++) { // apply the shadow to the shadowMap if (reflect) { _los[xTile + ((yTileOffset)*ySign)][yTile + ((currentShadow + xTileOffset) * xSign)] |= shadowType; } else { _los[xTile + ((currentShadow + xTileOffset) * xSign)][yTile + ((yTileOffset)*ySign)] |= shadowType; } } xTileOffset += shadowLength; } // for (int currentSegment = 0; currentSegment < shadowRaster[currentRaster][0]; currentSegment++) //======================================== } // if (viewportTiles[xTile][yTile].front().getTileType()->isOpaque()) } // for (int currentRow = 0; currentRow <= currentCol; currentRow++) } // for (int currentCol = 1; currentCol <= _NUM_RASTERS_COLS; currentCol++) } // for (octant = 0; octant < _OCTANTS; octant++) // go through all tiles on the viewable area and set the appropriate visibility for (y = 0; y < VIEWPORT_H; y++) { for (x = 0; x < VIEWPORT_W; x++) { // if the shadow flags equal __VCH, hide it, otherwise it's fully visible // if ((_los[x][y] & __VCH) == __VCH) { _los[x][y] = 0; } else { _los[x][y] = 1; } } } } void Screen::screenGetLineTerms(int x1, int y1, int x2, int y2, double *a, double *b) { if (x2 - x1 == 0) { *a = DBL_MAX; *b = x1; } else { *a = ((double)(y2 - y1)) / ((double)(x2 - x1)); *b = y1 - ((*a) * x1); } } int Screen::screenPointsOnSameSideOfLine(int x1, int y1, int x2, int y2, double a, double b) { double p1, p2; if (a == DBL_MAX) { p1 = x1 - b; p2 = x2 - b; } else { p1 = x1 * a + b - y1; p2 = x2 * a + b - y2; } if ((p1 > 0.0 && p2 > 0.0) || (p1 < 0.0 && p2 < 0.0) || (p1 == 0.0 && p2 == 0.0)) return 1; return 0; } int Screen::screenPointInTriangle(int x, int y, int tx1, int ty1, int tx2, int ty2, int tx3, int ty3) { double a[3], b[3]; screenGetLineTerms(tx1, ty1, tx2, ty2, &(a[0]), &(b[0])); screenGetLineTerms(tx2, ty2, tx3, ty3, &(a[1]), &(b[1])); screenGetLineTerms(tx3, ty3, tx1, ty1, &(a[2]), &(b[2])); if (!screenPointsOnSameSideOfLine(x, y, tx3, ty3, a[0], b[0])) return 0; if (!screenPointsOnSameSideOfLine(x, y, tx1, ty1, a[1], b[1])) return 0; if (!screenPointsOnSameSideOfLine(x, y, tx2, ty2, a[2], b[2])) return 0; return 1; } int Screen::screenPointInMouseArea(int x, int y, const MouseArea *area) { assertMsg(area->_nPoints == 2 || area->_nPoints == 3, "unsupported number of points in area: %d", area->_nPoints); /* two points define a rectangle */ if (area->_nPoints == 2) { if (x >= (int)(area->_point[0].x * settings._scale) && y >= (int)(area->_point[0].y * settings._scale) && x < (int)(area->_point[1].x * settings._scale) && y < (int)(area->_point[1].y * settings._scale)) { return 1; } } /* three points define a triangle */ else if (area->_nPoints == 3) { return screenPointInTriangle(x, y, area->_point[0].x * settings._scale, area->_point[0].y * settings._scale, area->_point[1].x * settings._scale, area->_point[1].y * settings._scale, area->_point[2].x * settings._scale, area->_point[2].y * settings._scale); } return 0; } void Screen::screenRedrawMapArea() { g_game->_mapArea.update(); } void Screen::screenEraseMapArea() { Image *screen = imageMgr->get("screen")->_image; screen->fillRect(BORDER_WIDTH * settings._scale, BORDER_WIDTH * settings._scale, VIEWPORT_W * TILE_WIDTH * settings._scale, VIEWPORT_H * TILE_HEIGHT * settings._scale, 0, 0, 0); } void Screen::screenEraseTextArea(int x, int y, int width, int height) { Image *screen = imageMgr->get("screen")->_image; screen->fillRect(x * CHAR_WIDTH * settings._scale, y * CHAR_HEIGHT * settings._scale, width * CHAR_WIDTH * settings._scale, height * CHAR_HEIGHT * settings._scale, 0, 0, 0); } void Screen::screenShake(int iterations) { if (settings._screenShakes) { // specify the size of the shake const int SHAKE_OFFSET = 1 * settings._scale; for (int i = 0; i < iterations; ++i) { // Shift the screen down g_system->setShakePos(0, SHAKE_OFFSET); g_system->updateScreen(); EventHandler::sleep(settings._shakeInterval); // shift the screen back up g_system->setShakePos(0, 0); g_system->updateScreen(); EventHandler::sleep(settings._shakeInterval); } } } void Screen::screenShowGemTile(Layout *layout, Map *map, MapTile &t, bool focus, int x, int y) { // Make sure we account for tiles that look like other tiles (dungeon tiles, mainly) Common::String looks_like = t.getTileType()->getLooksLike(); if (!looks_like.empty()) t = map->_tileSet->getByName(looks_like)->getId(); uint tile = map->translateToRawTileIndex(t); if (map->_type == Map::DUNGEON) { assertMsg(_charSetInfo, "charset not initialized"); Common::HashMap::iterator charIndex = _dungeonTileChars.find(t.getTileType()->getName()); if (charIndex != _dungeonTileChars.end()) { _charSetInfo->_image->drawSubRect((layout->_viewport.left + (x * layout->_tileShape.x)) * settings._scale, (layout->_viewport.top + (y * layout->_tileShape.y)) * settings._scale, 0, charIndex->_value * layout->_tileShape.y * settings._scale, layout->_tileShape.x * settings._scale, layout->_tileShape.y * settings._scale); } } else { if (_gemTilesInfo == nullptr) { _gemTilesInfo = imageMgr->get(BKGD_GEMTILES); if (!_gemTilesInfo) error("ERROR 1002: Unable to load the \"%s\" data file", BKGD_GEMTILES); } if (tile < 128) { _gemTilesInfo->_image->drawSubRect((layout->_viewport.left + (x * layout->_tileShape.x)) * settings._scale, (layout->_viewport.top + (y * layout->_tileShape.y)) * settings._scale, 0, tile * layout->_tileShape.y * settings._scale, layout->_tileShape.x * settings._scale, layout->_tileShape.y * settings._scale); } else { Image *screen = imageMgr->get("screen")->_image; screen->fillRect((layout->_viewport.left + (x * layout->_tileShape.x)) * settings._scale, (layout->_viewport.top + (y * layout->_tileShape.y)) * settings._scale, layout->_tileShape.x * settings._scale, layout->_tileShape.y * settings._scale, 0, 0, 0); } } } Layout *Screen::screenGetGemLayout(const Map *map) { if (map->_type == Map::DUNGEON) { for (auto *layout : _layouts) { if (layout->_type == LAYOUT_DUNGEONGEM) return layout; } error("no dungeon gem layout found!\n"); return nullptr; } else return _gemLayout; } void Screen::screenGemUpdate() { MapTile tile; int x, y; Image *screen = imageMgr->get("screen")->_image; screen->fillRect(BORDER_WIDTH * settings._scale, BORDER_HEIGHT * settings._scale, VIEWPORT_W * TILE_WIDTH * settings._scale, VIEWPORT_H * TILE_HEIGHT * settings._scale, 0, 0, 0); Layout *layout = screenGetGemLayout(g_context->_location->_map); // TODO: Move the code responsible for determining 'peer' visibility to a non SDL specific part of the code. if (g_context->_location->_map->_type == Map::DUNGEON) { //DO THE SPECIAL DUNGEON MAP TRAVERSAL Std::vector > drawnTiles(layout->_viewport.width(), Std::vector(layout->_viewport.height(), 0)); Common::List > coordStack; //Put the avatar's position on the stack int center_x = layout->_viewport.width() / 2 - 1; int center_y = layout->_viewport.height() / 2 - 1; int avt_x = g_context->_location->_coords.x - 1; int avt_y = g_context->_location->_coords.y - 1; coordStack.push_back(Common::Pair(center_x, center_y)); bool weAreDrawingTheAvatarTile = true; //And draw each tile on the growing stack until it is empty while (coordStack.size() > 0) { Common::Pair currentXY = coordStack.back(); coordStack.pop_back(); x = currentXY.first; y = currentXY.second; if (x < 0 || x >= layout->_viewport.width() || y < 0 || y >= layout->_viewport.height()) continue; //Skip out of range tiles if (drawnTiles[x][y]) continue; //Skip already considered tiles drawnTiles[x][y] = 1; // DRAW THE ACTUAL TILE bool focus; Std::vector tiles = screenViewportTile(layout->_viewport.width(), layout->_viewport.height(), x - center_x + avt_x, y - center_y + avt_y, focus); tile = tiles.front(); TileId avatarTileId = g_context->_location->_map->_tileSet->getByName("avatar")->getId(); if (!weAreDrawingTheAvatarTile) { //Hack to avoid showing the avatar tile multiple times in cycling dungeon maps if (tile.getId() == avatarTileId) tile = g_context->_location->_map->getTileFromData(g_context->_location->_coords)->getId(); } screenShowGemTile(layout, g_context->_location->_map, tile, focus, x, y); if (!tile.getTileType()->isOpaque() || tile.getTileType()->isWalkable() || weAreDrawingTheAvatarTile) { //Continue the search so we can see through all walkable objects, non-opaque objects (like creatures) //or the avatar position in those rare circumstances where he is stuck in a wall //by adding all relative adjacency combinations to the stack for drawing coordStack.push_back(Common::Pair(x + 1, y - 1)); coordStack.push_back(Common::Pair(x + 1, y)); coordStack.push_back(Common::Pair(x + 1, y + 1)); coordStack.push_back(Common::Pair(x, y - 1)); coordStack.push_back(Common::Pair(x, y + 1)); coordStack.push_back(Common::Pair(x - 1, y - 1)); coordStack.push_back(Common::Pair(x - 1, y)); coordStack.push_back(Common::Pair(x - 1, y + 1)); // We only draw the avatar tile once, it is the first tile drawn weAreDrawingTheAvatarTile = false; } } } else { // DO THE REGULAR EVERYTHING-IS-VISIBLE MAP TRAVERSAL for (x = 0; x < layout->_viewport.width(); x++) { for (y = 0; y < layout->_viewport.height(); y++) { bool focus; tile = screenViewportTile(layout->_viewport.width(), layout->_viewport.height(), x, y, focus) .front(); screenShowGemTile(layout, g_context->_location->_map, tile, focus, x, y); } } } screenRedrawMapArea(); screenUpdateCursor(); screenUpdateMoons(); screenUpdateWind(); } void Screen::screenRedrawTextArea(int x, int y, int width, int height) { g_system->updateScreen(); } void Screen::screenWait(int numberOfAnimationFrames) { update(); g_system->delayMillis(numberOfAnimationFrames * SCREEN_FRAME_TIME); } Image *Screen::screenScale(Image *src, int scale, int n, int filter) { Image *dest = nullptr; bool isTransparent; uint transparentIndex; bool alpha = src->isAlphaOn(); if (n == 0) n = 1; isTransparent = src->getTransparentIndex(transparentIndex); src->alphaOff(); while (filter && _filterScaler && (scale % 2 == 0)) { dest = (*_filterScaler)(src, 2, n); src = dest; scale /= 2; } if (scale == 3 && scaler3x(settings._filter)) { dest = (*_filterScaler)(src, 3, n); src = dest; scale /= 3; } if (scale != 1) dest = (*scalerGet("point"))(src, scale, n); if (!dest) dest = Image::duplicate(src, src->format()); if (isTransparent) dest->setTransparentIndex(transparentIndex); if (alpha) src->alphaOn(); return dest; } Image *Screen::screenScaleDown(Image *src, int scale) { int x, y; Image *dest; bool isTransparent; uint transparentIndex; bool alpha = src->isAlphaOn(); isTransparent = src->getTransparentIndex(transparentIndex); src->alphaOff(); dest = Image::create(src->width() / scale, src->height() / scale, src->format()); if (!dest) return nullptr; if (dest->isIndexed()) dest->setPaletteFromImage(src); for (y = 0; y < src->height(); y += scale) { for (x = 0; x < src->width(); x += scale) { uint index; src->getPixelIndex(x, y, index); dest->putPixelIndex(x / scale, y / scale, index); } } if (isTransparent) dest->setTransparentIndex(transparentIndex); if (alpha) src->alphaOn(); return dest; } #ifdef IOS_ULTIMA4 //Unsure if implementation required in iOS. void inline screenLock(){}; void inline screenUnlock(){}; void inline screenWait(int numberOfAnimationFrames){}; #endif const Std::vector &screenGetFilterNames() { return g_screen->_filterNames; } const Std::vector &screenGetGemLayoutNames() { return g_screen->_gemLayoutNames; } const Std::vector &screenGetLineOfSightStyles() { return g_screen->_lineOfSightStyles; } } // End of namespace Ultima4 } // End of namespace Ultima