mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-21 09:21:08 +00:00
542 lines
17 KiB
C++
542 lines
17 KiB
C++
/* 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 2
|
|
* 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* This code is based on original Hugo Trilogy source code
|
|
*
|
|
* Copyright (c) 1989-1995 David P. Gray
|
|
*
|
|
*/
|
|
|
|
// Find shortest route from hero to destination
|
|
|
|
#include "common/debug.h"
|
|
#include "common/system.h"
|
|
|
|
#include "hugo/hugo.h"
|
|
#include "hugo/game.h"
|
|
#include "hugo/route.h"
|
|
#include "hugo/object.h"
|
|
#include "hugo/inventory.h"
|
|
#include "hugo/mouse.h"
|
|
|
|
namespace Hugo {
|
|
Route::Route(HugoEngine *vm) : _vm(vm) {
|
|
_oldWalkDirection = 0;
|
|
_routeIndex = -1; // Hero not following a route
|
|
_routeType = kRouteSpace; // Hero walking to space
|
|
_routeObjId = -1; // Hero not walking to anything
|
|
|
|
for (int i = 0; i < kMaxSeg; i++)
|
|
_segment[i]._y = _segment[i]._x1 = _segment[i]._x2 = 0;
|
|
|
|
_segmentNumb = 0;
|
|
_routeListIndex = 0;
|
|
_destX = _destY = 0;
|
|
_heroWidth = 0;
|
|
_routeFoundFl = false;
|
|
_fullStackFl = false;
|
|
_fullSegmentFl = false;
|
|
}
|
|
|
|
void Route::resetRoute() {
|
|
_routeIndex = -1;
|
|
}
|
|
|
|
int16 Route::getRouteIndex() const {
|
|
return _routeIndex;
|
|
}
|
|
|
|
/**
|
|
* Face hero in new direction, based on cursor key input by user.
|
|
*/
|
|
void Route::setDirection(const uint16 keyCode) {
|
|
debugC(1, kDebugRoute, "setDirection(%d)", keyCode);
|
|
|
|
Object *obj = _vm->_hero; // Pointer to hero object
|
|
|
|
// Set first image in sequence
|
|
switch (keyCode) {
|
|
case Common::KEYCODE_UP:
|
|
case Common::KEYCODE_KP8:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_UP]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_DOWN:
|
|
case Common::KEYCODE_KP2:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_DOWN]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_LEFT:
|
|
case Common::KEYCODE_KP4:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_LEFT]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_RIGHT:
|
|
case Common::KEYCODE_KP6:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_RIGHT]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_HOME:
|
|
case Common::KEYCODE_KP7:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_LEFT]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_END:
|
|
case Common::KEYCODE_KP1:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_LEFT]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_PAGEUP:
|
|
case Common::KEYCODE_KP9:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_RIGHT]._seqPtr;
|
|
break;
|
|
case Common::KEYCODE_PAGEDOWN:
|
|
case Common::KEYCODE_KP3:
|
|
obj->_currImagePtr = obj->_seqList[SEQ_RIGHT]._seqPtr;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set hero walking, based on cursor key input by user.
|
|
* Hitting same key twice will stop hero.
|
|
*/
|
|
void Route::setWalk(const uint16 direction) {
|
|
debugC(1, kDebugRoute, "setWalk(%d)", direction);
|
|
|
|
Object *obj = _vm->_hero; // Pointer to hero object
|
|
|
|
if (_vm->getGameStatus()._storyModeFl || obj->_pathType != kPathUser) // Make sure user has control
|
|
return;
|
|
|
|
if (!obj->_vx && !obj->_vy)
|
|
_oldWalkDirection = 0; // Fix for consistant restarts
|
|
|
|
if (direction != _oldWalkDirection) {
|
|
// Direction has changed
|
|
setDirection(direction); // Face new direction
|
|
obj->_vx = obj->_vy = 0;
|
|
switch (direction) { // And set correct velocity
|
|
case Common::KEYCODE_UP:
|
|
case Common::KEYCODE_KP8:
|
|
obj->_vy = -kStepDy;
|
|
break;
|
|
case Common::KEYCODE_DOWN:
|
|
case Common::KEYCODE_KP2:
|
|
obj->_vy = kStepDy;
|
|
break;
|
|
case Common::KEYCODE_LEFT:
|
|
case Common::KEYCODE_KP4:
|
|
obj->_vx = -kStepDx;
|
|
break;
|
|
case Common::KEYCODE_RIGHT:
|
|
case Common::KEYCODE_KP6:
|
|
obj->_vx = kStepDx;
|
|
break;
|
|
case Common::KEYCODE_HOME:
|
|
case Common::KEYCODE_KP7:
|
|
obj->_vx = -kStepDx;
|
|
// Note: in v1 Dos and v2 Dos, obj->vy is set to DY
|
|
obj->_vy = -kStepDy / 2;
|
|
break;
|
|
case Common::KEYCODE_END:
|
|
case Common::KEYCODE_KP1:
|
|
obj->_vx = -kStepDx;
|
|
// Note: in v1 Dos and v2 Dos, obj->vy is set to -DY
|
|
obj->_vy = kStepDy / 2;
|
|
break;
|
|
case Common::KEYCODE_PAGEUP:
|
|
case Common::KEYCODE_KP9:
|
|
obj->_vx = kStepDx;
|
|
// Note: in v1 Dos and v2 Dos, obj->vy is set to -DY
|
|
obj->_vy = -kStepDy / 2;
|
|
break;
|
|
case Common::KEYCODE_PAGEDOWN:
|
|
case Common::KEYCODE_KP3:
|
|
obj->_vx = kStepDx;
|
|
// Note: in v1 Dos and v2 Dos, obj->vy is set to DY
|
|
obj->_vy = kStepDy / 2;
|
|
break;
|
|
}
|
|
_oldWalkDirection = direction;
|
|
obj->_cycling = kCycleForward;
|
|
} else {
|
|
// Same key twice - halt hero
|
|
obj->_vy = 0;
|
|
obj->_vx = 0;
|
|
_oldWalkDirection = 0;
|
|
obj->_cycling = kCycleNotCycling;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursive algorithm! Searches from hero to dest_x, dest_y
|
|
* Find horizontal line segment about supplied point and recursively
|
|
* find line segments for each point above and below that segment.
|
|
* When destination point found in segment, start surfacing and leave
|
|
* a trail in segment[] from destination back to hero.
|
|
*
|
|
* Note: there is a bug which allows a route through a 1-pixel high
|
|
* narrow gap if between 2 segments wide enough for hero. To work
|
|
* around this, make sure any narrow gaps are 2 or more pixels high.
|
|
* An example of this was the blocking guard in Hugo1/Dead-End.
|
|
*/
|
|
void Route::segment(int16 x, int16 y) {
|
|
debugC(1, kDebugRoute, "segment(%d, %d)", x, y);
|
|
|
|
// Note: use of static - can't waste stack
|
|
static ImagePtr p; // Ptr to _boundaryMap[y]
|
|
static Segment *segPtr; // Ptr to segment
|
|
|
|
// Bomb out if stack exhausted
|
|
// Vinterstum: Is this just a safeguard, or actually used?
|
|
//_fullStackFl = _stackavail () < 256;
|
|
_fullStackFl = false;
|
|
|
|
// Find and fill on either side of point
|
|
p = _boundaryMap[y];
|
|
int16 x1, x2; // Range of segment
|
|
for (x1 = x; x1 > 0; x1--) {
|
|
if (p[x1] == 0) {
|
|
p[x1] = kMapFill;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
for (x2 = x + 1; x2 < kXPix; x2++) {
|
|
if (p[x2] == 0) {
|
|
p[x2] = kMapFill;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
x1++;
|
|
x2--;
|
|
|
|
// Discard path if not wide enough for hero - dead end
|
|
if (_heroWidth > x2 - x1 + 1)
|
|
return;
|
|
|
|
// Have we found the destination yet?
|
|
if (y == _destY && x1 <= _destX && x2 >= _destX)
|
|
_routeFoundFl = true;
|
|
|
|
// Bounds check y in case no boundary around screen
|
|
if (y <= 0 || y >= kYPix - 1)
|
|
return;
|
|
|
|
if (_vm->_hero->_x < x1) {
|
|
// Hero x not in segment, search x1..x2
|
|
// Find all segments above current
|
|
for (x = x1; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x <= x2; x++) {
|
|
if (_boundaryMap[y - 1][x] == 0)
|
|
segment(x, y - 1);
|
|
}
|
|
|
|
// Find all segments below current
|
|
for (x = x1; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x <= x2; x++) {
|
|
if (_boundaryMap[y + 1][x] == 0)
|
|
segment(x, y + 1);
|
|
}
|
|
} else if (_vm->_hero->_x + kHeroMaxWidth > x2) {
|
|
// Hero x not in segment, search x1..x2
|
|
// Find all segments above current
|
|
for (x = x2; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x >= x1; x--) {
|
|
if (_boundaryMap[y - 1][x] == 0)
|
|
segment(x, y - 1);
|
|
}
|
|
|
|
// Find all segments below current
|
|
for (x = x2; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x >= x1; x--) {
|
|
if (_boundaryMap[y + 1][x] == 0)
|
|
segment(x, y + 1);
|
|
}
|
|
} else {
|
|
// Organize search around hero x position - this gives
|
|
// better chance for more direct route.
|
|
for (x = _vm->_hero->_x; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x <= x2; x++) {
|
|
if (_boundaryMap[y - 1][x] == 0)
|
|
segment(x, y - 1);
|
|
}
|
|
|
|
for (x = x1; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x < _vm->_hero->_x; x++) {
|
|
if (_boundaryMap[y - 1][x] == 0)
|
|
segment(x, y - 1);
|
|
}
|
|
|
|
for (x = _vm->_hero->_x; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x <= x2; x++) {
|
|
if (_boundaryMap[y + 1][x] == 0)
|
|
segment(x, y + 1);
|
|
}
|
|
|
|
for (x = x1; !(_routeFoundFl || _fullStackFl || _fullSegmentFl) && x < _vm->_hero->_x; x++) {
|
|
if (_boundaryMap[y + 1][x] == 0)
|
|
segment(x, y + 1);
|
|
}
|
|
}
|
|
|
|
// If found, surface, leaving trail back to hero
|
|
if (_routeFoundFl) {
|
|
// Bomb out if too many segments (leave one spare)
|
|
if (_segmentNumb >= kMaxSeg - 1) {
|
|
_fullSegmentFl = true;
|
|
} else {
|
|
// Create segment
|
|
segPtr = &_segment[_segmentNumb];
|
|
segPtr->_y = y;
|
|
segPtr->_x1 = x1;
|
|
segPtr->_x2 = x2;
|
|
_segmentNumb++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create and return ptr to new node. Initialize with previous node.
|
|
* Returns 0 if MAX_NODES exceeded
|
|
*/
|
|
Common::Point *Route::newNode() {
|
|
debugC(1, kDebugRoute, "newNode");
|
|
|
|
_routeListIndex++;
|
|
if (_routeListIndex >= kMaxNodes) // Too many nodes
|
|
return nullptr; // Incomplete route - failure
|
|
|
|
_route[_routeListIndex] = _route[_routeListIndex - 1]; // Initialize with previous node
|
|
return &_route[_routeListIndex];
|
|
}
|
|
|
|
/**
|
|
* Construct route to cx, cy. Return TRUE if successful.
|
|
* 1. Copy boundary bitmap to local byte map (include object bases)
|
|
* 2. Construct list of segments segment[] from hero to destination
|
|
* 3. Compress to shortest route in route[]
|
|
*/
|
|
bool Route::findRoute(const int16 cx, const int16 cy) {
|
|
debugC(1, kDebugRoute, "findRoute(%d, %d)", cx, cy);
|
|
|
|
// Initialize for search
|
|
_routeFoundFl = false; // Path not found yet
|
|
_fullStackFl = false; // Stack not exhausted
|
|
_fullSegmentFl = false; // Segments not exhausted
|
|
_segmentNumb = 0; // Segment index
|
|
_heroWidth = kHeroMinWidth; // Minimum width of hero
|
|
_destY = cy; // Destination coords
|
|
_destX = cx; // Destination coords
|
|
|
|
int16 herox1 = _vm->_hero->_x + _vm->_hero->_currImagePtr->_x1; // Hero baseline
|
|
int16 herox2 = _vm->_hero->_x + _vm->_hero->_currImagePtr->_x2; // Hero baseline
|
|
int16 heroy = _vm->_hero->_y + _vm->_hero->_currImagePtr->_y2; // Hero baseline
|
|
|
|
// Store all object baselines into objbound (except hero's = [0])
|
|
Object *obj; // Ptr to object
|
|
int i;
|
|
for (i = 1, obj = &_vm->_object->_objects[i]; i < _vm->_object->_numObj; i++, obj++) {
|
|
if ((obj->_screenIndex == *_vm->_screenPtr) && (obj->_cycling != kCycleInvisible) && (obj->_priority == kPriorityFloating))
|
|
_vm->_object->storeBoundary(obj->_oldx + obj->_currImagePtr->_x1, obj->_oldx + obj->_currImagePtr->_x2, obj->_oldy + obj->_currImagePtr->_y2);
|
|
}
|
|
|
|
// Combine objbound and boundary bitmaps to local byte map
|
|
for (uint16 y = 0; y < kYPix; y++) {
|
|
for (uint16 x = 0; x < kCompLineSize; x++) {
|
|
uint16 boundIdx = y * kCompLineSize + x;
|
|
for (i = 0; i < 8; i++)
|
|
_boundaryMap[y][x * 8 + i] = ((_vm->_object->getObjectBoundary(boundIdx) | _vm->_object->getBoundaryOverlay(boundIdx)) & (0x80 >> i)) ? kMapBound : 0;
|
|
}
|
|
}
|
|
|
|
// Clear all object baselines from objbound
|
|
for (i = 0, obj = _vm->_object->_objects; i < _vm->_object->_numObj; i++, obj++) {
|
|
if ((obj->_screenIndex == *_vm->_screenPtr) && (obj->_cycling != kCycleInvisible) && (obj->_priority == kPriorityFloating))
|
|
_vm->_object->clearBoundary(obj->_oldx + obj->_currImagePtr->_x1, obj->_oldx + obj->_currImagePtr->_x2, obj->_oldy + obj->_currImagePtr->_y2);
|
|
}
|
|
|
|
// Search from hero to destination
|
|
segment(herox1, heroy);
|
|
|
|
// Not found or not enough stack or MAX_SEG exceeded
|
|
if (!_routeFoundFl || _fullStackFl || _fullSegmentFl) {
|
|
return false;
|
|
}
|
|
|
|
// Now find the route of nodes from destination back to hero
|
|
// Assign first node as destination
|
|
_route[0].x = _destX;
|
|
_route[0].y = _destY;
|
|
|
|
// Make a final segment for hero's base (we left a spare)
|
|
_segment[_segmentNumb]._y = heroy;
|
|
_segment[_segmentNumb]._x1 = herox1;
|
|
_segment[_segmentNumb]._x2 = herox2;
|
|
_segmentNumb++;
|
|
|
|
// Look in segments[] for straight lines from destination to hero
|
|
for (i = 0, _routeListIndex = 0; i < _segmentNumb - 1; i++) {
|
|
Common::Point *routeNode; // Ptr to route node
|
|
if ((routeNode = newNode()) == 0) // New node for new segment
|
|
return false; // Too many nodes
|
|
routeNode->y = _segment[i]._y;
|
|
|
|
// Look ahead for furthest straight line
|
|
for (int16 j = i + 1; j < _segmentNumb; j++) {
|
|
Segment *segPtr = &_segment[j];
|
|
// Can we get to this segment from previous node?
|
|
if (segPtr->_x1 <= routeNode->x && segPtr->_x2 >= routeNode->x + _heroWidth - 1) {
|
|
routeNode->y = segPtr->_y; // Yes, keep updating node
|
|
} else {
|
|
// No, create another node on previous segment to reach it
|
|
if ((routeNode = newNode()) == 0) // Add new route node
|
|
return false; // Too many nodes
|
|
|
|
// Find overlap between old and new segments
|
|
int16 x1 = MAX(_segment[j - 1]._x1, segPtr->_x1);
|
|
int16 x2 = MIN(_segment[j - 1]._x2, segPtr->_x2);
|
|
|
|
// If room, add a little offset to reduce staircase effect
|
|
int16 dx = kHeroMaxWidth >> 1;
|
|
if (x2 - x1 < _heroWidth + dx)
|
|
dx = 0;
|
|
|
|
// Bear toward final hero position
|
|
if (j == _segmentNumb - 1)
|
|
routeNode->x = herox1;
|
|
else if (herox1 < x1)
|
|
routeNode->x = x1 + dx;
|
|
else if (herox1 > x2 - _heroWidth + 1)
|
|
routeNode->x = x2 - _heroWidth - dx;
|
|
else
|
|
routeNode->x = herox1;
|
|
i = j - 2; // Restart segment (-1 to offset auto increment)
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Terminate loop if we've reached hero
|
|
if (routeNode->x == herox1 && routeNode->y == heroy)
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Process hero in route mode - called from Move_objects()
|
|
*/
|
|
void Route::processRoute() {
|
|
debugC(1, kDebugRoute, "processRoute");
|
|
|
|
static bool turnedFl = false; // Used to get extra cycle for turning
|
|
|
|
if (_routeIndex < 0)
|
|
return;
|
|
|
|
// Current hero position
|
|
int16 herox = _vm->_hero->_x + _vm->_hero->_currImagePtr->_x1;
|
|
int16 heroy = _vm->_hero->_y + _vm->_hero->_currImagePtr->_y2;
|
|
Common::Point *routeNode = &_route[_routeIndex];
|
|
|
|
// Arrived at node?
|
|
if (abs(herox - routeNode->x) < kStepDx + 1 && abs(heroy - routeNode->y) < kStepDy) {
|
|
// kStepDx too low
|
|
// Close enough - position hero exactly
|
|
_vm->_hero->_x = _vm->_hero->_oldx = routeNode->x - _vm->_hero->_currImagePtr->_x1;
|
|
_vm->_hero->_y = _vm->_hero->_oldy = routeNode->y - _vm->_hero->_currImagePtr->_y2;
|
|
_vm->_hero->_vx = _vm->_hero->_vy = 0;
|
|
_vm->_hero->_cycling = kCycleNotCycling;
|
|
|
|
// Arrived at final node?
|
|
if (--_routeIndex < 0) {
|
|
// See why we walked here
|
|
switch (_routeType) {
|
|
case kRouteExit: // Walked to an exit, proceed into it
|
|
setWalk(_vm->_mouse->getDirection(_routeObjId));
|
|
break;
|
|
case kRouteLook: // Look at an object
|
|
if (turnedFl) {
|
|
_vm->_object->lookObject(&_vm->_object->_objects[_routeObjId]);
|
|
turnedFl = false;
|
|
} else {
|
|
setDirection(_vm->_object->_objects[_routeObjId]._direction);
|
|
_routeIndex++; // Come round again
|
|
turnedFl = true;
|
|
}
|
|
break;
|
|
case kRouteGet: // Get (or use) an object
|
|
if (turnedFl) {
|
|
_vm->_object->useObject(_routeObjId);
|
|
turnedFl = false;
|
|
} else {
|
|
setDirection(_vm->_object->_objects[_routeObjId]._direction);
|
|
_routeIndex++; // Come round again
|
|
turnedFl = true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
} else if (_vm->_hero->_vx == 0 && _vm->_hero->_vy == 0) {
|
|
// Set direction of travel if at a node
|
|
// Note realignment when changing to (thinner) up/down sprite,
|
|
// otherwise hero could bump into boundaries along route.
|
|
if (herox < routeNode->x) {
|
|
setWalk(Common::KEYCODE_RIGHT);
|
|
} else if (herox > routeNode->x) {
|
|
setWalk(Common::KEYCODE_LEFT);
|
|
} else if (heroy < routeNode->y) {
|
|
setWalk(Common::KEYCODE_DOWN);
|
|
_vm->_hero->_x = _vm->_hero->_oldx = routeNode->x - _vm->_hero->_currImagePtr->_x1;
|
|
} else if (heroy > routeNode->y) {
|
|
setWalk(Common::KEYCODE_UP);
|
|
_vm->_hero->_x = _vm->_hero->_oldx = routeNode->x - _vm->_hero->_currImagePtr->_x1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start a new route from hero to cx, cy
|
|
* go_for is the purpose, id indexes the exit or object to walk to
|
|
* Returns FALSE if route not found
|
|
*/
|
|
bool Route::startRoute(const RouteType routeType, const int16 objId, int16 cx, int16 cy) {
|
|
debugC(1, kDebugRoute, "startRoute(%d, %d, %d, %d)", routeType, objId, cx, cy);
|
|
|
|
// Don't attempt to walk if user does not have control
|
|
if (_vm->_hero->_pathType != kPathUser)
|
|
return false;
|
|
|
|
// if inventory showing, make it go away
|
|
if (_vm->_inventory->getInventoryState() != kInventoryOff)
|
|
_vm->_inventory->setInventoryState(kInventoryUp);
|
|
|
|
_routeType = routeType; // Purpose of trip
|
|
_routeObjId = objId; // Index of exit/object
|
|
|
|
// Adjust destination to center hero if walking to cursor
|
|
if (_routeType == kRouteSpace)
|
|
cx -= kHeroMinWidth / 2;
|
|
|
|
bool foundFl = false; // TRUE if route found ok
|
|
if ((foundFl = findRoute(cx, cy))) { // Found a route?
|
|
_routeIndex = _routeListIndex; // Node index
|
|
_vm->_hero->_vx = _vm->_hero->_vy = 0; // Stop manual motion
|
|
}
|
|
|
|
return foundFl;
|
|
}
|
|
|
|
} // End of namespace Hugo
|