Implemented text drawing and cleanup.
svn-id: r31756
This commit is contained in:
parent
6d3a7e4f6b
commit
fc6fe46951
5 changed files with 266 additions and 64 deletions
|
@ -225,7 +225,7 @@ int FontResource::getHeight() const {
|
|||
return _data[0];
|
||||
}
|
||||
|
||||
int FontResource::getCharWidth(char c) const {
|
||||
int FontResource::getCharWidth(uint c) const {
|
||||
byte *charData = getCharData(c);
|
||||
if (charData)
|
||||
return charData[0];
|
||||
|
@ -233,7 +233,7 @@ int FontResource::getCharWidth(char c) const {
|
|||
return 0;
|
||||
}
|
||||
|
||||
byte *FontResource::getChar(char c) const {
|
||||
byte *FontResource::getChar(uint c) const {
|
||||
byte *charData = getCharData(c);
|
||||
if (charData)
|
||||
return charData + 1;
|
||||
|
@ -241,7 +241,17 @@ byte *FontResource::getChar(char c) const {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
byte *FontResource::getCharData(char c) const {
|
||||
int FontResource::getTextWidth(const char *text) {
|
||||
int width = 0;
|
||||
if (text) {
|
||||
int len = strlen(text);
|
||||
for (int pos = 0; pos < len; pos++)
|
||||
width += getCharWidth(text[pos]);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
byte *FontResource::getCharData(uint c) const {
|
||||
if (c < 28 || c > 255)
|
||||
return NULL;
|
||||
return _data + 1 + (c - 28) * (getHeight() + 1);
|
||||
|
|
|
@ -118,12 +118,13 @@ public:
|
|||
~FontResource();
|
||||
void load(byte *source, int size);
|
||||
int getHeight() const;
|
||||
int getCharWidth(char c) const;
|
||||
byte *getChar(char c) const;
|
||||
int getCharWidth(uint c) const;
|
||||
int getTextWidth(const char *text);
|
||||
byte *getChar(uint c) const;
|
||||
protected:
|
||||
byte *_data;
|
||||
int _size;
|
||||
byte *getCharData(char c) const;
|
||||
byte *getCharData(uint c) const;
|
||||
};
|
||||
|
||||
class XmidiResource : public Resource {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include "made/made.h"
|
||||
#include "made/screen.h"
|
||||
#include "made/resource.h"
|
||||
#include "made/database.h"
|
||||
|
||||
namespace Made {
|
||||
|
||||
|
@ -66,8 +67,13 @@ Screen::Screen(MadeEngine *vm) : _vm(vm) {
|
|||
|
||||
_textX = 0;
|
||||
_textY = 0;
|
||||
_textColor = 0;
|
||||
_textRect.left = 0;
|
||||
_textRect.top = 0;
|
||||
_textRect.right = 320;
|
||||
_textRect.bottom = 200;
|
||||
_font = NULL;
|
||||
_currentFontIndex = 0;
|
||||
_currentFontNum = 0;
|
||||
_fontDrawCtx.x = 0;
|
||||
_fontDrawCtx.y = 0;
|
||||
_fontDrawCtx.w = 320;
|
||||
|
@ -206,7 +212,7 @@ void Screen::drawSpriteChannels(const ClipInfo &clipInfo, int16 includeStateMask
|
|||
break;
|
||||
|
||||
case 2: // drawObjectText
|
||||
// TODO
|
||||
printObjectText(_channels[i].index, _channels[i].x, _channels[i].y, _channels[i].fontNum, _channels[i].textColor, _channels[i].outlineColor, clipInfo);
|
||||
break;
|
||||
|
||||
case 3: // drawAnimFrame
|
||||
|
@ -454,7 +460,63 @@ int16 Screen::getAnimFrameCount(uint16 animIndex) {
|
|||
|
||||
|
||||
uint16 Screen::placeText(uint16 channelIndex, uint16 textObjectIndex, int16 x, int16 y, uint16 fontNum, int16 textColor, int16 outlineColor) {
|
||||
return 0;
|
||||
|
||||
if (channelIndex < 1 || channelIndex >= 100 || textObjectIndex == 0 || fontNum == 0)
|
||||
return 0;
|
||||
|
||||
channelIndex--;
|
||||
|
||||
Object *obj = _vm->_dat->getObject(textObjectIndex);
|
||||
const char *text = obj->getString();
|
||||
|
||||
int16 x1, y1, x2, y2;
|
||||
|
||||
setFont(fontNum);
|
||||
|
||||
int textWidth = _font->getTextWidth(text);
|
||||
int textHeight = _font->getHeight();
|
||||
|
||||
if (outlineColor != -1) {
|
||||
textWidth += 2;
|
||||
textHeight += 2;
|
||||
x--;
|
||||
y--;
|
||||
}
|
||||
|
||||
x1 = x;
|
||||
y1 = y;
|
||||
x2 = x + textWidth;
|
||||
y2 = y + textHeight;
|
||||
//TODO: clipRect(x1, y1, x2, y2);
|
||||
|
||||
if (textWidth > 0 && outlineColor != -1) {
|
||||
x++;
|
||||
y++;
|
||||
}
|
||||
|
||||
int16 state = 1;
|
||||
|
||||
if (_ground == 0)
|
||||
state |= 2;
|
||||
|
||||
_channels[channelIndex].state = state;
|
||||
_channels[channelIndex].type = 2;
|
||||
_channels[channelIndex].index = textObjectIndex;
|
||||
_channels[channelIndex].x = x;
|
||||
_channels[channelIndex].y = y;
|
||||
_channels[channelIndex].textColor = textColor;
|
||||
_channels[channelIndex].fontNum = fontNum;
|
||||
_channels[channelIndex].outlineColor = outlineColor;
|
||||
_channels[channelIndex].x1 = x1;
|
||||
_channels[channelIndex].y1 = y1;
|
||||
_channels[channelIndex].x2 = x2;
|
||||
_channels[channelIndex].y2 = y2;
|
||||
_channels[channelIndex].area = (x2 - x2) * (y2 - y1);
|
||||
|
||||
if (_channelsUsedCount <= channelIndex)
|
||||
_channelsUsedCount = channelIndex + 1;
|
||||
|
||||
return channelIndex + 1;
|
||||
}
|
||||
|
||||
void Screen::show() {
|
||||
|
@ -465,7 +527,6 @@ void Screen::show() {
|
|||
return;
|
||||
|
||||
drawSpriteChannels(_clipInfo1, 3, 0);
|
||||
|
||||
memcpy(_screen2->pixels, _screen1->pixels, 64000);
|
||||
drawSpriteChannels(_clipInfo2, 1, 2);
|
||||
|
||||
|
@ -499,16 +560,16 @@ void Screen::flash(int flashCount) {
|
|||
}
|
||||
}
|
||||
|
||||
void Screen::setFont(int16 fontIndex) {
|
||||
if (fontIndex == _currentFontIndex)
|
||||
void Screen::setFont(int16 fontNum) {
|
||||
if (fontNum == _currentFontNum)
|
||||
return;
|
||||
if (_font)
|
||||
_vm->_res->freeResource(_font);
|
||||
_font = _vm->_res->getFont(fontIndex);
|
||||
_currentFontIndex = fontIndex;
|
||||
_font = _vm->_res->getFont(fontNum);
|
||||
_currentFontNum = fontNum;
|
||||
}
|
||||
|
||||
void Screen::printChar(char c, int16 x, int16 y, byte color) {
|
||||
void Screen::printChar(uint c, int16 x, int16 y, byte color) {
|
||||
|
||||
if (!_font)
|
||||
return;
|
||||
|
@ -534,4 +595,123 @@ void Screen::printChar(char c, int16 x, int16 y, byte color) {
|
|||
|
||||
}
|
||||
|
||||
void Screen::printText(const char *text) {
|
||||
|
||||
const int tabWidth = 5;
|
||||
|
||||
if (!_font)
|
||||
return;
|
||||
|
||||
int textLen = strlen(text);
|
||||
int textHeight = _font->getHeight();
|
||||
int linePos = 1;
|
||||
int16 x = _textX;
|
||||
int16 y = _textY;
|
||||
|
||||
for (int textPos = 0; textPos < textLen; textPos++) {
|
||||
|
||||
uint c = text[textPos];
|
||||
int charWidth = _font->getCharWidth(c);
|
||||
|
||||
if (c == 9) {
|
||||
linePos = ((linePos / tabWidth) + 1) * tabWidth;
|
||||
x = _textRect.left + linePos * _font->getCharWidth(32);
|
||||
} else if (c == 10) {
|
||||
linePos = 1;
|
||||
x = _textRect.left;
|
||||
y += textHeight;
|
||||
} else if (c == 13) {
|
||||
linePos = 1;
|
||||
x = _textRect.left;
|
||||
} else if (c == 32) {
|
||||
// TODO: Word-wrap
|
||||
int wrapPos = textPos + 1;
|
||||
int wrapX = x + charWidth;
|
||||
while (wrapPos < textLen && text[wrapPos] != 0 && text[wrapPos] != 32 && text[wrapPos] >= 28) {
|
||||
wrapX += _font->getCharWidth(text[wrapPos]);
|
||||
wrapPos++;
|
||||
}
|
||||
if (wrapX >= _textRect.right) {
|
||||
linePos = 1;
|
||||
x = _textRect.left;
|
||||
y += textHeight;
|
||||
charWidth = 0;
|
||||
// TODO: text[textPos] = '\x01';
|
||||
}
|
||||
}
|
||||
|
||||
if (x + charWidth > _textRect.right) {
|
||||
linePos = 1;
|
||||
x = _textRect.left;
|
||||
y += textHeight;
|
||||
}
|
||||
|
||||
if (y + textHeight > _textRect.bottom) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (c >= 28 && c <= 255) {
|
||||
if (_dropShadowColor != -1) {
|
||||
printChar(c, x + 1, y + 1, _dropShadowColor);
|
||||
}
|
||||
if (_outlineColor != -1) {
|
||||
printChar(c, x, y - 1, _outlineColor);
|
||||
printChar(c, x, y + 1, _outlineColor);
|
||||
printChar(c, x - 1, y, _outlineColor);
|
||||
printChar(c, x + 1, y, _outlineColor);
|
||||
printChar(c, x - 1, y - 1, _outlineColor);
|
||||
printChar(c, x - 1, y + 1, _outlineColor);
|
||||
printChar(c, x + 1, y - 1, _outlineColor);
|
||||
printChar(c, x + 1, y + 1, _outlineColor);
|
||||
}
|
||||
printChar(c, x, y, _textColor);
|
||||
x += charWidth;
|
||||
linePos++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_textX = x;
|
||||
_textY = y;
|
||||
|
||||
}
|
||||
|
||||
void Screen::printTextEx(const char *text, int16 x, int16 y, int16 fontNum, int16 textColor, int16 outlineColor, const ClipInfo &clipInfo) {
|
||||
if (*text == 0 || x == 0 || y == 0)
|
||||
return;
|
||||
|
||||
int16 oldFontNum = _currentFontNum;
|
||||
Common::Rect oldTextRect;
|
||||
|
||||
_fontDrawCtx = clipInfo;
|
||||
|
||||
getTextRect(oldTextRect);
|
||||
setFont(fontNum);
|
||||
setTextColor(textColor);
|
||||
setOutlineColor(outlineColor);
|
||||
setTextXY(x, y);
|
||||
printText(text);
|
||||
setTextRect(oldTextRect);
|
||||
setFont(oldFontNum);
|
||||
|
||||
}
|
||||
|
||||
void Screen::printObjectText(int16 objectIndex, int16 x, int16 y, int16 fontNum, int16 textColor, int16 outlineColor, const ClipInfo &clipInfo) {
|
||||
|
||||
if (objectIndex == 0)
|
||||
return;
|
||||
|
||||
Object *obj = _vm->_dat->getObject(objectIndex);
|
||||
const char *text = obj->getString();
|
||||
|
||||
printTextEx(text, x, y, fontNum, textColor, outlineColor, clipInfo);
|
||||
|
||||
}
|
||||
|
||||
int16 Screen::getTextWidth(int16 fontNum, const char *text) {
|
||||
setFont(fontNum);
|
||||
return _font->getTextWidth(text);
|
||||
}
|
||||
|
||||
|
||||
} // End of namespace Made
|
||||
|
|
|
@ -74,6 +74,16 @@ public:
|
|||
void setGround(uint16 ground) { _ground = ground; }
|
||||
void setTextColor(int16 color) { _textColor = color; }
|
||||
|
||||
void setTextRect(const Common::Rect &textRect) {
|
||||
_textRect = textRect;
|
||||
_textX = _textRect.left;
|
||||
_textY = _textRect.top;
|
||||
}
|
||||
|
||||
void getTextRect(Common::Rect &textRect) {
|
||||
textRect = _textRect;
|
||||
}
|
||||
|
||||
void setOutlineColor(int16 color) {
|
||||
_outlineColor = color;
|
||||
_dropShadowColor = -1;
|
||||
|
@ -84,6 +94,11 @@ public:
|
|||
_dropShadowColor = color;
|
||||
}
|
||||
|
||||
void setTextXY(int16 x, int16 y) {
|
||||
_textX = x;
|
||||
_textY = y;
|
||||
}
|
||||
|
||||
uint16 updateChannel(uint16 channelIndex);
|
||||
void deleteChannel(uint16 channelIndex);
|
||||
int16 getChannelType(uint16 channelIndex);
|
||||
|
@ -117,8 +132,12 @@ public:
|
|||
void show();
|
||||
void flash(int count);
|
||||
|
||||
void setFont(int16 fontIndex);
|
||||
void printChar(char c, int16 x, int16 y, byte color);
|
||||
void setFont(int16 fontNum);
|
||||
void printChar(uint c, int16 x, int16 y, byte color);
|
||||
void printText(const char *text);
|
||||
void printTextEx(const char *text, int16 x, int16 y, int16 fontNum, int16 textColor, int16 outlineColor, const ClipInfo &clipInfo);
|
||||
void printObjectText(int16 objectIndex, int16 x, int16 y, int16 fontNum, int16 textColor, int16 outlineColor, const ClipInfo &clipInfo);
|
||||
int16 getTextWidth(int16 fontNum, const char *text);
|
||||
|
||||
|
||||
protected:
|
||||
|
@ -131,13 +150,13 @@ protected:
|
|||
byte _palette[768], _newPalette[768], _fxPalette[768];
|
||||
int _paletteColorCount, _oldPaletteColorCount;
|
||||
bool _paletteInitialized, _needPalette;
|
||||
uint16 _currentFont;
|
||||
int16 _textColor;
|
||||
int16 _outlineColor;
|
||||
int16 _dropShadowColor;
|
||||
|
||||
int16 _textX, _textY;
|
||||
int16 _currentFontIndex;
|
||||
Common::Rect _textRect;
|
||||
int16 _currentFontNum;
|
||||
FontResource *_font;
|
||||
ClipInfo _fontDrawCtx;
|
||||
|
||||
|
|
|
@ -165,23 +165,22 @@ void ScriptFunctionsRtz::setupExternalsTable() {
|
|||
#undef External
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_SYSTEM(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_SYSTEM");
|
||||
// This opcode is empty.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_INITGRAF(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_INITGRAF");
|
||||
// This opcode is empty.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_RESTOREGRAF(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_RESTOREGRAF");
|
||||
// This opcode is empty.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_DRAWPIC(int16 argc, int16 *argv) {
|
||||
int16 channel = _vm->_screen->drawPic(argv[4], argv[3], argv[2], argv[1], argv[0]);
|
||||
return channel;
|
||||
return _vm->_screen->drawPic(argv[4], argv[3], argv[2], argv[1], argv[0]);
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_CLS(int16 argc, int16 *argv) {
|
||||
|
@ -252,7 +251,6 @@ int16 ScriptFunctionsRtz::o1_EVENT(int16 argc, int16 *argv) {
|
|||
}
|
||||
|
||||
_vm->_system->updateScreen();
|
||||
//g_system->delayMillis(10);
|
||||
|
||||
return eventNum;
|
||||
}
|
||||
|
@ -275,18 +273,18 @@ int16 ScriptFunctionsRtz::o1_VISUALFX(int16 argc, int16 *argv) {
|
|||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_PLAYSND(int16 argc, int16 *argv) {
|
||||
int soundId = argv[0];
|
||||
int soundNum = argv[0];
|
||||
bool loop = false;
|
||||
|
||||
if (argc > 1) {
|
||||
soundId = argv[1];
|
||||
soundNum = argv[1];
|
||||
loop = (argv[0] == 1);
|
||||
}
|
||||
|
||||
if (soundId > 0) {
|
||||
if (soundNum > 0) {
|
||||
if (!_vm->_mixer->isSoundHandleActive(_audioStreamHandle)) {
|
||||
_vm->_mixer->playInputStream(Audio::Mixer::kPlainSoundType, &_audioStreamHandle,
|
||||
_vm->_res->getSound(soundId)->getAudioStream(_vm->_soundRate, loop));
|
||||
_vm->_res->getSound(soundNum)->getAudioStream(_vm->_soundRate, loop));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,9 +292,9 @@ int16 ScriptFunctionsRtz::o1_PLAYSND(int16 argc, int16 *argv) {
|
|||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_PLAYMUS(int16 argc, int16 *argv) {
|
||||
int16 musicId = argv[0];
|
||||
if (musicId > 0) {
|
||||
XmidiResource *xmidi = _vm->_res->getXmidi(musicId);
|
||||
int16 musicNum = argv[0];
|
||||
if (musicNum > 0) {
|
||||
XmidiResource *xmidi = _vm->_res->getXmidi(musicNum);
|
||||
_vm->_music->play(xmidi);
|
||||
_vm->_res->freeResource(xmidi);
|
||||
}
|
||||
|
@ -317,6 +315,8 @@ int16 ScriptFunctionsRtz::o1_ISMUS(int16 argc, int16 *argv) {
|
|||
|
||||
int16 ScriptFunctionsRtz::o1_TEXTPOS(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_TEXTPOS");
|
||||
// This seems to be some kind of low-level opcode.
|
||||
// The original engine calls int 10h to set the VGA cursor position.
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -421,16 +421,14 @@ int16 ScriptFunctionsRtz::o1_PALETTELOCK(int16 argc, int16 *argv) {
|
|||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_FONT(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_FONT");
|
||||
|
||||
uint16 fontID = argv[0];
|
||||
printf("Set font to %i\n", fontID);
|
||||
_vm->_screen->setFont(fontID);
|
||||
_vm->_screen->setFont(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_DRAWTEXT(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_DRAWTEXT");
|
||||
Object *obj = _vm->_dat->getObject(argv[argc - 1]);
|
||||
warning("argc = %d; drawText = %s", argc, obj->getString());
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -441,26 +439,20 @@ int16 ScriptFunctionsRtz::o1_HOMETEXT(int16 argc, int16 *argv) {
|
|||
|
||||
int16 ScriptFunctionsRtz::o1_TEXTRECT(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_TEXTRECT");
|
||||
|
||||
int16 x1 = CLIP<int16>(argv[0], 1, 318);
|
||||
int16 y1 = CLIP<int16>(argv[1], 1, 198);
|
||||
int16 x1 = CLIP<int16>(argv[4], 1, 318);
|
||||
int16 y1 = CLIP<int16>(argv[3], 1, 198);
|
||||
int16 x2 = CLIP<int16>(argv[2], 1, 318);
|
||||
int16 y2 = CLIP<int16>(argv[3], 1, 198);
|
||||
int16 textValue = argv[4];
|
||||
|
||||
printf("Text rect: %i, %i, %i, %i - text value: %i\n", x1, y1, x2, y2, textValue);
|
||||
// TODO: set text rect
|
||||
|
||||
int16 y2 = CLIP<int16>(argv[1], 1, 198);
|
||||
int16 textValue = argv[0];
|
||||
// TODO: textValue
|
||||
_vm->_screen->setTextRect(Common::Rect(x1, y1, x2, y2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_TEXTXY(int16 argc, int16 *argv) {
|
||||
warning("Unimplemented opcode: o1_TEXTXY");
|
||||
|
||||
int16 x = CLIP<int16>(argv[0], 1, 318);
|
||||
int16 y = CLIP<int16>(argv[1], 1, 198);
|
||||
|
||||
printf("Text: x = %i, y = %i\n", x, y);
|
||||
int16 x = CLIP<int16>(argv[1], 1, 318);
|
||||
int16 y = CLIP<int16>(argv[0], 1, 198);
|
||||
_vm->_screen->setTextXY(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -574,6 +566,7 @@ int16 ScriptFunctionsRtz::o1_MONOCLS(int16 argc, int16 *argv) {
|
|||
int16 ScriptFunctionsRtz::o1_SNDENERGY(int16 argc, int16 *argv) {
|
||||
// This is called while in-game voices are played
|
||||
// Not sure what it's used for
|
||||
// -> It's used to animate mouths when NPCs are talking
|
||||
// Commented out to reduce spam
|
||||
//warning("Unimplemented opcode: o1_SNDENERGY");
|
||||
return 0;
|
||||
|
@ -590,11 +583,13 @@ int16 ScriptFunctionsRtz::o1_ANIMTEXT(int16 argc, int16 *argv) {
|
|||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_TEXTWIDTH(int16 argc, int16 *argv) {
|
||||
Object *obj = _vm->_dat->getObject(argv[1]);
|
||||
const char *text = obj->getString();
|
||||
debug(4, "text = %s\n", text);
|
||||
// TODO
|
||||
return 0;
|
||||
int16 width = 0;
|
||||
if (argv[1] > 0) {
|
||||
Object *obj = _vm->_dat->getObject(argv[1]);
|
||||
const char *text = obj->getString();
|
||||
width = _vm->_screen->getTextWidth(argv[0], text);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_PLAYMOVIE(int16 argc, int16 *argv) {
|
||||
|
@ -645,10 +640,7 @@ int16 ScriptFunctionsRtz::o1_PLACESPRITE(int16 argc, int16 *argv) {
|
|||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_PLACETEXT(int16 argc, int16 *argv) {
|
||||
Object *obj = _vm->_dat->getObject(argv[5]);
|
||||
const char *text = obj->getString();
|
||||
debug(4, "text = %s\n", text); fflush(stdout);
|
||||
return 0;
|
||||
return _vm->_screen->placeText(argv[6], argv[5], argv[4], argv[3], argv[2], argv[1], argv[0]);
|
||||
}
|
||||
|
||||
int16 ScriptFunctionsRtz::o1_DELETECHANNEL(int16 argc, int16 *argv) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue