Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
23 KB
Referenced Files
None
Subscribers
None
diff --git a/src/Scenery.cpp b/src/Scenery.cpp
index b7b71b7..c6a6a37 100644
--- a/src/Scenery.cpp
+++ b/src/Scenery.cpp
@@ -1,389 +1,401 @@
/*
* Copyright (C) 2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me And My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "GameObjects.h"
#include "Scenery.h"
#include "Functions.h"
#include "LevelEditor.h"
#include "POASerializer.h"
#include <iostream>
#include <sstream>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
#include "libs/tinyformat/tinyformat.h"
Scenery::Scenery(Game* objParent) :
GameObject(objParent),
xSave(0),
ySave(0),
dx(0),
dy(0),
themeBlock(NULL),
repeatMode(0)
{}
Scenery::Scenery(Game* objParent, int x, int y, int w, int h, const std::string& sceneryName) :
GameObject(objParent),
xSave(0),
ySave(0),
dx(0),
dy(0),
themeBlock(NULL),
repeatMode(0)
{
box.x = boxBase.x = x;
box.y = boxBase.y = y;
box.w = boxBase.w = w;
box.h = boxBase.h = h;
if (sceneryName.empty()) {
themeBlock = &internalThemeBlock;
} else {
// Load the appearance.
themeBlock = objThemes.getScenery(sceneryName);
if (themeBlock) {
sceneryName_ = sceneryName;
} else {
fprintf(stderr, "ERROR: Can't find scenery with name '%s'.\n", sceneryName.c_str());
themeBlock = &internalThemeBlock;
}
}
themeBlock->createInstance(&appearance);
}
Scenery::~Scenery(){
//Destroy the themeBlock since it isn't needed anymore.
internalThemeBlock.destroy();
}
-static inline int getNewCoord(unsigned char rm, int default_, int cameraX, int cameraW, int levelW) {
+static inline int getNewCoord(unsigned char rm, int default_, int cameraX, int cameraW, int levelW, int offset) {
switch (rm) {
case Scenery::NEGATIVE_INFINITY:
return cameraX;
case Scenery::ZERO:
- return std::max(cameraX, 0);
+ return std::max(cameraX, offset);
case Scenery::LEVEL_SIZE:
- return std::min(cameraX + cameraW, levelW);
+ return std::min(cameraX + cameraW, levelW + offset);
case Scenery::POSITIVE_INFINITY:
return cameraX + cameraW;
default:
return default_;
}
}
-void Scenery::show(SDL_Renderer& renderer){
+void Scenery::show(SDL_Renderer& renderer) {
+ showScenery(renderer, 0, 0);
+}
+
+void Scenery::showScenery(SDL_Renderer& renderer, int offsetX, int offsetY) {
+ //The box which is offset by the input.
+ const SDL_Rect box = {
+ this->box.x + offsetX,
+ this->box.y + offsetY,
+ this->box.w,
+ this->box.h,
+ };
+
//The real box according to repeat mode.
SDL_Rect theBox = {
- getNewCoord(repeatMode, box.x, camera.x, camera.w, LEVEL_WIDTH),
- getNewCoord(repeatMode >> 16, box.y, camera.y, camera.h, LEVEL_HEIGHT),
- getNewCoord(repeatMode >> 8, box.x + box.w, camera.x, camera.w, LEVEL_WIDTH),
- getNewCoord(repeatMode >> 24, box.y + box.h, camera.y, camera.h, LEVEL_HEIGHT),
+ getNewCoord(repeatMode, box.x, camera.x, camera.w, LEVEL_WIDTH, offsetX),
+ getNewCoord(repeatMode >> 16, box.y, camera.y, camera.h, LEVEL_HEIGHT, offsetX),
+ getNewCoord(repeatMode >> 8, box.x + box.w, camera.x, camera.w, LEVEL_WIDTH, offsetY),
+ getNewCoord(repeatMode >> 24, box.y + box.h, camera.y, camera.h, LEVEL_HEIGHT, offsetY),
};
theBox.w -= theBox.x;
theBox.h -= theBox.y;
//Check if the scenery is visible.
if (theBox.w > 0 && theBox.h > 0 && checkCollision(camera, theBox)) {
//Snap the size to integral multiple of box.w and box.h
if (box.w > 1) {
theBox.w += theBox.x;
if (repeatMode & 0xFFu) {
theBox.x = box.x + int(floor(float(theBox.x - box.x) / float(box.w))) * box.w;
}
if (repeatMode & 0xFF00u) {
theBox.w = box.x + int(ceil(float(theBox.w - box.x) / float(box.w))) * box.w;
}
theBox.w -= theBox.x;
}
if (box.h > 1) {
theBox.h += theBox.y;
if (repeatMode & 0xFF0000u) {
theBox.y = box.y + int(floor(float(theBox.y - box.y) / float(box.h))) * box.h;
}
if (repeatMode & 0xFF000000u) {
theBox.h = box.y + int(ceil(float(theBox.h - box.y) / float(box.h))) * box.h;
}
theBox.h -= theBox.y;
}
//Now draw normal.
if (theBox.w > 0 && theBox.h > 0) {
appearance.draw(renderer, theBox.x - camera.x, theBox.y - camera.y, theBox.w, theBox.h);
}
}
//Draw some stupid icons in edit mode.
if (stateID == STATE_LEVEL_EDITOR && checkCollision(camera, box)) {
auto bmGUI = static_cast<LevelEditor*>(parent)->getGuiTexture();
if (!bmGUI) {
return;
}
int x = box.x - camera.x + 2;
//Draw a stupid icon for custom scenery.
if (themeBlock == &internalThemeBlock) {
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
//Draw a stupid icon for horizonal repeat.
if (repeatMode & 0x0000FFFFu) {
const SDL_Rect r = { 64, 32, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
//Draw a stupid icon for vertical repeat.
if (repeatMode & 0xFFFF0000u) {
const SDL_Rect r = { 64, 48, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
}
}
SDL_Rect Scenery::getBox(int boxType){
SDL_Rect r={0,0,0,0};
switch(boxType){
case BoxType_Base:
return boxBase;
case BoxType_Previous:
r.x=box.x-dx;
r.y=box.y-dy;
r.w=box.w;
r.h=box.h;
return r;
case BoxType_Delta:
r.x=dx;
r.y=dy;
return r;
case BoxType_Velocity:
return r;
case BoxType_Current:
return box;
}
return r;
}
void Scenery::setLocation(int x,int y){
//The scenery has moved so calculate the delta.
dx=x-box.x;
dy=y-box.y;
//And set its new location.
box.x=x;
box.y=y;
}
void Scenery::saveState(){
//Store the location.
xSave=box.x-boxBase.x;
ySave=box.y-boxBase.y;
//And any animations.
appearance.saveAnimation();
}
void Scenery::loadState(){
//Restore the location.
box.x=boxBase.x+xSave;
box.y=boxBase.y+ySave;
//And load the animation.
appearance.loadAnimation();
}
void Scenery::reset(bool save){
//Reset the scenery to its original location.
box.x=boxBase.x;
box.y=boxBase.y;
if(save)
xSave=ySave=0;
//Also reset the appearance.
appearance.resetAnimation(save);
appearance.changeState("default");
//NOTE: We load the animation right after changing it to prevent a transition.
if(save)
appearance.loadAnimation();
}
void Scenery::playAnimation(){}
void Scenery::onEvent(int eventType){
//NOTE: Scenery should not interact with the player or vice versa.
}
int Scenery::queryProperties(int propertyType,Player* obj){
//NOTE: Scenery doesn't have any properties.
return 0;
}
void Scenery::getEditorData(std::vector<std::pair<std::string,std::string> >& obj){
obj.push_back(pair<string, string>("sceneryName", sceneryName_));
obj.push_back(pair<string, string>("customScenery", customScenery_));
obj.push_back(pair<string, string>("repeatMode", tfm::format("%d", repeatMode)));
}
void Scenery::setEditorData(std::map<std::string,std::string>& obj){
// NOTE: currently the sceneryName cannot be changed by this method.
auto it = obj.find("customScenery");
if (it != obj.end()) {
customScenery_ = it->second;
}
it = obj.find("repeatMode");
if (it != obj.end()) {
repeatMode = atoi(it->second.c_str());
}
}
std::string Scenery::getEditorProperty(std::string property){
//First get the complete editor data.
vector<pair<string,string> > objMap;
vector<pair<string,string> >::iterator it;
getEditorData(objMap);
//Loop through the entries.
for(it=objMap.begin();it!=objMap.end();++it){
if(it->first==property)
return it->second;
}
//Nothing found.
return "";
}
void Scenery::setEditorProperty(std::string property,std::string value){
//Create a map to hold the property.
std::map<std::string,std::string> editorData;
editorData[property]=value;
//And call the setEditorData method.
setEditorData(editorData);
}
bool Scenery::loadFromNode(ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* objNode){
sceneryName_.clear();
customScenery_.clear();
repeatMode = 0;
if (objNode->name == "object") {
//Make sure there are enough arguments.
if (objNode->value.size() < 2)
return false;
//Load position and size.
box.x = boxBase.x = atoi(objNode->value[0].c_str());
box.y = boxBase.y = atoi(objNode->value[1].c_str());
box.w = boxBase.w = (objNode->value.size() >= 3) ? atoi(objNode->value[2].c_str()) : 50;
box.h = boxBase.h = (objNode->value.size() >= 4) ? atoi(objNode->value[3].c_str()) : 50;
//Dump the current TreeStorageNode.
//NOTE: we temporarily remove all attributes since they are not related to theme.
std::map<std::string, std::vector<std::string> > tmpAttributes;
std::swap(objNode->attributes, tmpAttributes);
std::ostringstream o;
POASerializer().writeNode(objNode, o, false, true);
customScenery_ = o.str();
//restore old attributes
std::swap(objNode->attributes, tmpAttributes);
//Load the appearance.
if (!internalThemeBlock.loadFromNode(objNode, levels->levelpackPath, imageManager, renderer)) return false;
themeBlock = &internalThemeBlock;
themeBlock->createInstance(&appearance);
} else if (objNode->name == "scenery") {
//Make sure there are enough arguments.
if (objNode->value.size() < 3)
return false;
//Load position and size.
box.x = boxBase.x = atoi(objNode->value[1].c_str());
box.y = boxBase.y = atoi(objNode->value[2].c_str());
box.w = boxBase.w = (objNode->value.size() >= 4) ? atoi(objNode->value[3].c_str()) : 50;
box.h = boxBase.h = (objNode->value.size() >= 5) ? atoi(objNode->value[4].c_str()) : 50;
//Load the appearance.
themeBlock = objThemes.getScenery(objNode->value[0]);
if (!themeBlock) {
fprintf(stderr, "ERROR: Can't find scenery with name '%s'.\n", objNode->value[0].c_str());
return false;
}
themeBlock->createInstance(&appearance);
//Save the scenery name.
sceneryName_ = objNode->value[0];
} else {
//Unsupported node name for scenery block
fprintf(stderr, "ERROR: Unsupported node name '%s' for scenery block.\n", objNode->name.c_str());
return false;
}
auto it = objNode->attributes.find("repeatMode");
if (it != objNode->attributes.end() && it->second.size() >= 4) {
repeatMode = atoi(it->second[0].c_str())
| (atoi(it->second[1].c_str()) << 8)
| (atoi(it->second[2].c_str()) << 16)
| (atoi(it->second[3].c_str()) << 24);
}
return true;
}
bool Scenery::updateCustomScenery(ImageManager& imageManager, SDL_Renderer& renderer) {
POASerializer serializer;
std::istringstream i(customScenery_);
TreeStorageNode objNode;
//Load the node from text dump
if (!serializer.readNode(i, &objNode, true)) return false;
//Load the appearance.
if (!internalThemeBlock.loadFromNode(&objNode, levels->levelpackPath, imageManager, renderer)) return false;
themeBlock = &internalThemeBlock;
themeBlock->createInstance(&appearance);
// Clear the scenery name since we are using custom scenery
sceneryName_.clear();
return true;
}
void Scenery::prepareFrame(){
//Reset the delta variables.
dx=dy=0;
}
void Scenery::move(){
//Update our appearance.
appearance.updateAnimation();
}
diff --git a/src/Scenery.h b/src/Scenery.h
index fbdd123..1f01f44 100644
--- a/src/Scenery.h
+++ b/src/Scenery.h
@@ -1,149 +1,154 @@
/*
* Copyright (C) 2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SCENERY_H
#define SCENERY_H
#include "GameObjects.h"
#include "ThemeManager.h"
#include <vector>
#include <SDL.h>
class Scenery: public GameObject{
private:
//Save variables for the current location of the scenery.
int xSave,ySave;
//Delta variables, if the scenery moves these must be set to the delta movement.
int dx,dy;
public:
//The ThemeBlock, kept so it can be deleted later on.
ThemeBlock internalThemeBlock;
// The pointer points to the real ThemeBlock, either point to internalThemeBlock, or a ThemeBlock in ThemeManager, or NULL.
ThemeBlock* themeBlock;
//The Appearance of the scenery.
//NOTE: We use a ThemeBlockInstance since it allows for all sorts of things like animations.
ThemeBlockInstance appearance;
// The scenery name. "" means custom scenery, in this case themeBlock is pointing to internalThemeBlock
std::string sceneryName_;
// The custom scenery description, which is the text dump of the TreeStorageNode.
std::string customScenery_;
// The repeat mode.
enum RepeatMode {
DEFAULT, // Starts or ends at the position of this block (default)
NEGATIVE_INFINITY, // Starts at negative infinity
ZERO, // Starts or ends at 0
LEVEL_SIZE, // Starts or ends at level size
POSITIVE_INFINITY, // Ends at positive infinity
REPEAT_MODE_MAX,
};
// The repeat mode of this block. The value is Scenery::RepeatMode left shifted by appropriate value
// bit 0-7: x start
// bit 8-15: x end
// bit 16-23: y start
// bit 24-31: y end
unsigned int repeatMode;
//Constructor.
//objParent: Pointer to the Game object.
Scenery(Game* objParent);
//Constructor.
//objParent: Pointer to the Game object.
//x: the x coordinate
//y: the y coordinate
//w: the width
//h: the height
//sceneryName: the scenery name, "" means custom scenery block
Scenery(Game* objParent, int x, int y, int w, int h, const std::string& sceneryName);
//Desturctor.
~Scenery();
//Method to load custom scenery from customScenery_ member variable.
bool updateCustomScenery(ImageManager& imageManager, SDL_Renderer& renderer);
//Method used to draw the scenery.
+ //NOTE: To enable parallax scrolling, etc. use showScenery() instead.
void show(SDL_Renderer& renderer) override;
+ //Method used to draw the scenery.
+ //offsetX/Y: the offset apply to the scenery block before considering camera position.
+ void showScenery(SDL_Renderer& renderer, int offsetX, int offsetY);
+
//Returns the box of a given type.
//boxType: The type of box that should be returned.
//See GameObjects.h for the types.
//Returns: The box.
virtual SDL_Rect getBox(int boxType=BoxType_Current);
//Method used to set the location of the scenery.
//NOTE: The new location isn't stored as base location.
//x: The new x location.
//y: The new y location.
virtual void setLocation(int x,int y);
//Save the state of the scenery so we can load it later on.
virtual void saveState();
//Load the saved state of the scenery.
virtual void loadState();
//Reset the scenery.
//save: Boolean if the saved state should also be deleted.
virtual void reset(bool save);
//Play an animation.
virtual void playAnimation();
//Method called when there's an event.
//eventType: The type of event.
//See GameObjects.h for the eventtypes.
virtual void onEvent(int eventType);
//Method used to retrieve a property from the scenery.
//propertyType: The type of property requested.
//See GameObjects.h for the properties.
//obj: Pointer to the player.
//Returns: Integer containing the value of the property.
virtual int queryProperties(int propertyType,Player* obj);
//Get the editor data of the scenery.
//obj: The vector that will be filled with the editorData.
virtual void getEditorData(std::vector<std::pair<std::string,std::string> >& obj);
//Set the editor data of the scenery.
//obj: The new editor data.
virtual void setEditorData(std::map<std::string,std::string>& obj);
//Get a single property of the scenery.
//property: The property to return.
//Returns: The value for the requested property.
virtual std::string getEditorProperty(std::string property);
//Set a single property of the scenery.
//property: The property to set.
//value: The new value for the property.
virtual void setEditorProperty(std::string property,std::string value);
//Method for loading the Scenery object from a node.
//objNode: Pointer to the storage node to load from.
//Returns: True if it succeeds without errors.
virtual bool loadFromNode(ImageManager& imageManager,SDL_Renderer& renderer,TreeStorageNode* objNode) override;
//Method used for resetting the dx/dy and xVel/yVel variables.
virtual void prepareFrame();
//Method used for updating any animations.
virtual void move();
};
#endif
diff --git a/src/SceneryLayer.cpp b/src/SceneryLayer.cpp
index 728b806..01e3ab8 100644
--- a/src/SceneryLayer.cpp
+++ b/src/SceneryLayer.cpp
@@ -1,194 +1,202 @@
/*
* Copyright (C) 2018 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SceneryLayer.h"
#include "ImageManager.h"
#include "Render.h"
#include "POASerializer.h"
+#include "LevelEditor.h"
#include <stdio.h>
#include <stdlib.h>
+#include <math.h>
#include <sstream>
SceneryLayer::SceneryLayer()
: speedX(0), speedY(0)
, cameraX(1), cameraY(1)
, currentX(0), currentY(0)
, savedX(0), savedY(0)
{
}
SceneryLayer::~SceneryLayer() {
for (auto o : objects) {
delete o;
}
objects.clear();
}
void SceneryLayer::loadFromNode(Game *parent, ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* obj) {
//Retrieve the speed.
{
auto &v = obj->attributes["speed"];
if (v.size() >= 2) {
speedX = atof(v[0].c_str());
speedY = atof(v[1].c_str());
}
}
//Retrieve the camera speed.
{
auto &v = obj->attributes["cameraSpeed"];
if (v.size() >= 2) {
cameraX = atof(v[0].c_str());
cameraY = atof(v[1].c_str());
}
}
//Loop through the sub nodes.
for (auto obj2 : obj->subNodes){
if (obj2 == NULL) continue;
if (obj2->name == "object" || obj2->name == "scenery"){
//Load the scenery from node.
Scenery* scenery = new Scenery(parent);
if (!scenery->loadFromNode(imageManager, renderer, obj2)){
delete scenery;
continue;
}
objects.push_back(scenery);
}
}
}
void SceneryLayer::updateAnimation() {
//Update the animation of the layer itself.
currentX += speedX;
currentY += speedY;
//Update the animation of objects.
for (auto obj : objects) {
obj->move();
}
}
void SceneryLayer::resetAnimation(bool save) {
currentX = 0.0f;
currentY = 0.0f;
if (save){
savedX = 0.0f;
savedY = 0.0f;
}
for (auto obj : objects) {
obj->reset(save);
}
}
void SceneryLayer::saveAnimation() {
savedX = currentX;
savedY = currentY;
for (auto obj : objects) {
obj->saveState();
}
}
void SceneryLayer::loadAnimation() {
currentX = savedX;
currentY = savedY;
for (auto obj : objects) {
obj->loadState();
}
}
void SceneryLayer::show(SDL_Renderer& renderer) {
- //TODO: offset the objects by currentX/Y, etc.
+ int offsetX = 0, offsetY = 0;
+
+ //Offset the objects by currentX/Y. Only in play mode.
+ if (stateID != STATE_LEVEL_EDITOR || dynamic_cast<LevelEditor*>(currentState)->isPlayMode()) {
+ offsetX = (int)floor(currentX + (1.0f - cameraX) * (float)camera.x + 0.5f);
+ offsetY = (int)floor(currentY + (1.0f - cameraY) * (float)camera.y + 0.5f);
+ }
//Show objects.
for (auto obj : objects) {
- obj->show(renderer);
+ obj->showScenery(renderer, offsetX, offsetY);
}
}
void SceneryLayer::saveToNode(TreeStorageNode* obj) {
char s[64];
//Save the speed.
sprintf(s, "%g", speedX);
obj->attributes["speed"].push_back(s);
sprintf(s, "%g", speedY);
obj->attributes["speed"].push_back(s);
//Save the camera speed.
sprintf(s, "%g", cameraX);
obj->attributes["cameraSpeed"].push_back(s);
sprintf(s, "%g", cameraY);
obj->attributes["cameraSpeed"].push_back(s);
//Loop through the scenery blocks and save them.
for (auto scenery : objects) {
TreeStorageNode* obj1 = new TreeStorageNode;
obj->subNodes.push_back(obj1);
// Check if it's custom scenery block
if (scenery->themeBlock == &(scenery->internalThemeBlock)) {
// load the dump of TreeStorageNode
POASerializer serializer;
std::istringstream i(scenery->customScenery_);
serializer.readNode(i, obj1, true);
// custom scenery
obj1->name = "object";
// clear the value in case that the serializer is buggy
obj1->value.clear();
// clear the attributes in case that the user inputs some attributes
obj1->attributes.clear();
} else {
// predefined scenery
obj1->name = "scenery";
//Write away the name of the scenery.
obj1->value.push_back(scenery->sceneryName_);
}
//Get the box for the location of the scenery.
SDL_Rect box = scenery->getBox(BoxType_Base);
//Put the location and size in the storageNode.
sprintf(s, "%d", box.x);
obj1->value.push_back(s);
sprintf(s, "%d", box.y);
obj1->value.push_back(s);
sprintf(s, "%d", box.w);
obj1->value.push_back(s);
sprintf(s, "%d", box.h);
obj1->value.push_back(s);
//Get the repeat mode of the scenery if it's not default value
if (scenery->repeatMode) {
std::vector<std::string> &v = obj1->attributes["repeatMode"];
for (int i = 0; i < 4; i++) {
sprintf(s, "%d", ((scenery->repeatMode) >> (i * 8)) & 0xFF);
v.push_back(s);
}
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 16, 8:28 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
63785
Default Alt Text
(23 KB)

Event Timeline