Commit 38e58ef6 authored by nanahira's avatar nanahira

Merge branch 'fh-20250210' into develop

# Conflicts:
#	gframe/data_manager.h
#	gframe/image_manager.cpp
#	gframe/image_manager.h
#	gframe/sound_manager.cpp
parents 8cc33752 7887bc71
......@@ -388,32 +388,26 @@ const wchar_t* DataManager::GetDesc(uint32_t strCode) const {
return csit->second.desc[offset].c_str();
return unknown_string;
}
const wchar_t* DataManager::GetSysString(int code) const {
auto csit = _sysStrings.find(code);
if(csit == _sysStrings.end())
const wchar_t* DataManager::GetMapString(const wstring_map& table, uint32_t code) const {
auto csit = table.find(code);
if (csit == table.end())
return unknown_string;
return csit->second.c_str();
}
const wchar_t* DataManager::GetVictoryString(int code) const {
auto csit = _victoryStrings.find(code);
if(csit == _victoryStrings.end())
return unknown_string;
return csit->second.c_str();
const wchar_t* DataManager::GetSysString(uint32_t code) const {
return GetMapString(_sysStrings, code);
}
const wchar_t* DataManager::GetCounterName(int code) const {
auto csit = _counterStrings.find(code);
if(csit == _counterStrings.end())
return unknown_string;
return csit->second.c_str();
const wchar_t* DataManager::GetVictoryString(uint32_t code) const {
return GetMapString(_victoryStrings, code);
}
const wchar_t* DataManager::GetSetName(int code) const {
auto csit = _setnameStrings.find(code);
if(csit == _setnameStrings.end())
return unknown_string;
return csit->second.c_str();
const wchar_t* DataManager::GetCounterName(uint32_t code) const {
return GetMapString(_counterStrings, code);
}
const wchar_t* DataManager::GetSetName(uint32_t code) const {
return GetMapString(_setnameStrings, code);
}
std::vector<unsigned int> DataManager::GetSetCodes(std::wstring setname) const {
std::vector<unsigned int> matchingCodes;
std::vector<uint32_t> DataManager::GetSetCodes(std::wstring setname) const {
std::vector<uint32_t> matchingCodes;
for(auto csit = _setnameStrings.begin(); csit != _setnameStrings.end(); ++csit) {
auto xpos = csit->second.find_first_of(L'|');//setname|another setname or extra info
if (mainGame->CheckRegEx(csit->second, setname, true)) {
......
......@@ -57,6 +57,7 @@ struct CardString {
};
using code_pointer = std::unordered_map<uint32_t, CardDataC>::const_iterator;
using string_pointer = std::unordered_map<uint32_t, CardString>::const_iterator;
using wstring_map = std::unordered_map<uint32_t, std::wstring>;
class ClientCard;
......@@ -95,11 +96,11 @@ public:
const wchar_t* GetName(uint32_t code) const;
const wchar_t* GetText(uint32_t code) const;
const wchar_t* GetDesc(uint32_t strCode) const;
const wchar_t* GetSysString(int code) const;
const wchar_t* GetVictoryString(int code) const;
const wchar_t* GetCounterName(int code) const;
const wchar_t* GetSetName(int code) const;
std::vector<unsigned int> GetSetCodes(std::wstring setname) const;
const wchar_t* GetSysString(uint32_t code) const;
const wchar_t* GetVictoryString(uint32_t code) const;
const wchar_t* GetCounterName(uint32_t code) const;
const wchar_t* GetSetName(uint32_t code) const;
std::vector<uint32_t> GetSetCodes(std::wstring setname) const;
std::wstring GetNumString(int num, bool bracket = false) const;
const wchar_t* FormatLocation(int location, int sequence) const;
const wchar_t* FormatLocation(ClientCard* card) const;
......@@ -109,19 +110,19 @@ public:
std::wstring FormatSetName(const uint16_t setcode[]) const;
std::wstring FormatLinkMarker(unsigned int link_marker) const;
std::unordered_map<unsigned int, std::wstring> _counterStrings;
std::unordered_map<unsigned int, std::wstring> _victoryStrings;
std::unordered_map<unsigned int, std::wstring> _setnameStrings;
std::unordered_map<unsigned int, std::wstring> _sysStrings;
std::vector<std::pair<std::wstring, std::wstring>> _serverStrings;
wstring_map _counterStrings;
wstring_map _victoryStrings;
wstring_map _setnameStrings;
wstring_map _sysStrings;
std::vector<std::pair<std::wstring, std::wstring>> _serverStrings;
char errmsg[512]{};
const wchar_t* unknown_string{ L"???" };
irr::io::IFileSystem* FileSystem{};
static constexpr int STRING_ID_LOCATION = 1000;
static constexpr int STRING_ID_ATTRIBUTE = 1010;
static constexpr int STRING_ID_RACE = 1020;
static constexpr int STRING_ID_TYPE = 1050;
static constexpr uint32_t STRING_ID_LOCATION = 1000;
static constexpr uint32_t STRING_ID_ATTRIBUTE = 1010;
static constexpr uint32_t STRING_ID_RACE = 1020;
static constexpr uint32_t STRING_ID_TYPE = 1050;
static constexpr int TYPES_COUNT = 27;
static unsigned char scriptBuffer[0x100000];
......@@ -141,6 +142,7 @@ public:
static bool deck_sort_name(code_pointer l1, code_pointer l2);
private:
const wchar_t* GetMapString(const wstring_map& table, uint32_t code) const;
std::unordered_map<uint32_t, CardDataC> _datas;
std::unordered_map<uint32_t, CardString> _strings;
std::unordered_map<uint32_t, std::vector<uint16_t>> extra_setcode;
......
......@@ -1440,7 +1440,7 @@ void DeckBuilder::FilterCards() {
results.clear();
struct element_t {
std::wstring keyword;
std::vector<unsigned int> setcodes;
std::vector<uint32_t> setcodes;
enum class type_t {
all,
name,
......
#include "image_manager.h"
#include "image_resizer.h"
#include "game.h"
#include "myfilesystem.h"
#include <thread>
#ifdef _OPENMP
#include <omp.h>
#endif
namespace ygo {
......@@ -213,84 +211,15 @@ void ImageManager::ResizeTexture() {
if(!tBackGround_deck)
tBackGround_deck = tBackGround;
}
/** Scale image using nearest neighbor anti-aliasing.
* Function by Warr1024, from https://github.com/minetest/minetest/issues/2419, modified. */
void imageScaleNNAA(irr::video::IImage *src, irr::video::IImage *dest) {
const auto& srcDim = src->getDimension();
const auto& destDim = dest->getDimension();
// Cache scale ratios.
const double rx = (double)srcDim.Width / destDim.Width;
const double ry = (double)srcDim.Height / destDim.Height;
#pragma omp parallel if(mainGame->gameConf.use_image_scale_multi_thread)
{
double sx, sy, minsx, maxsx, minsy, maxsy, area, ra, ga, ba, aa, pw, ph, pa;
irr::video::SColor pxl, npxl;
// Walk each destination image pixel.
#pragma omp for schedule(dynamic)
for(irr::s32 dy = 0; dy < (irr::s32)destDim.Height; dy++) {
for(irr::s32 dx = 0; dx < (irr::s32)destDim.Width; dx++) {
// Calculate floating-point source rectangle bounds.
minsx = dx * rx;
maxsx = minsx + rx;
minsy = dy * ry;
maxsy = minsy + ry;
// Total area, and integral of r, g, b values over that area,
// initialized to zero, to be summed up in next loops.
area = 0;
ra = 0;
ga = 0;
ba = 0;
aa = 0;
// Loop over the integral pixel positions described by those bounds.
for(sy = floor(minsy); sy < maxsy; sy++) {
for(sx = floor(minsx); sx < maxsx; sx++) {
// Calculate width, height, then area of dest pixel
// that's covered by this source pixel.
pw = 1;
if(minsx > sx)
pw += sx - minsx;
if(maxsx < (sx + 1))
pw += maxsx - sx - 1;
ph = 1;
if(minsy > sy)
ph += sy - minsy;
if(maxsy < (sy + 1))
ph += maxsy - sy - 1;
pa = pw * ph;
// Get source pixel and add it to totals, weighted
// by covered area and alpha.
pxl = src->getPixel((irr::u32)sx, (irr::u32)sy);
area += pa;
ra += pa * pxl.getRed();
ga += pa * pxl.getGreen();
ba += pa * pxl.getBlue();
aa += pa * pxl.getAlpha();
}
}
// Set the destination image pixel to the average color.
if(area > 0) {
npxl.set((irr::u32)(aa / area + 0.5),
(irr::u32)(ra / area + 0.5),
(irr::u32)(ga / area + 0.5),
(irr::u32)(ba / area + 0.5));
} else {
npxl.set(0);
}
dest->setPixel(dx, dy, npxl);
}
}
} // end of parallel region
void ImageManager::resizeImage(irr::video::IImage* src, irr::video::IImage* dest, bool use_threading) {
imageResizer.resize(src, dest, use_threading);
}
/** Convert image to texture, resizing if needed.
/**
* Convert image to texture, resizing if needed.
* @param name Texture name (Irrlicht texture key).
* @param srcimg Source image; will be dropped by this function.
* @return Texture pointer. Remove via `driver->removeTexture` (do not `drop`). */
* @return Texture pointer. Remove via `driver->removeTexture` (do not `drop`).
*/
irr::video::ITexture* ImageManager::addTexture(const char* name, irr::video::IImage* srcimg, irr::s32 width, irr::s32 height) {
if(srcimg == nullptr)
return nullptr;
......@@ -299,7 +228,7 @@ irr::video::ITexture* ImageManager::addTexture(const char* name, irr::video::IIm
texture = driver->addTexture(name, srcimg);
} else {
irr::video::IImage* destimg = driver->createImage(srcimg->getColorFormat(), irr::core::dimension2d<irr::u32>(width, height));
imageScaleNNAA(srcimg, destimg);
resizeImage(srcimg, destimg, mainGame->gameConf.use_image_scale_multi_thread);
texture = driver->addTexture(name, destimg);
destimg->drop();
}
......@@ -388,14 +317,14 @@ irr::video::ITexture* ImageManager::GetTexture(int code, bool fit) {
* @return Texture pointer. Should NOT be removed nor dropped. */
irr::video::ITexture* ImageManager::GetBigPicture(int code, float zoom) {
if(code == 0)
return tUnknown;
return tUnknownFit;
if(tBigPicture != nullptr) {
driver->removeTexture(tBigPicture);
tBigPicture = nullptr;
}
irr::video::IImage* img = GetImage(code);
if(img == nullptr) {
return tUnknown;
return tUnknownFit;
}
char name[256];
mysnprintf(name, "pics/%d/big", code);
......@@ -427,7 +356,7 @@ int ImageManager::LoadThumbThread() {
imageManager.tThumbLoadingMutex.unlock();
} else {
irr::video::IImage *destimg = imageManager.driver->createImage(img->getColorFormat(), irr::core::dimension2d<irr::u32>(width, height));
imageScaleNNAA(img, destimg);
imageManager.resizeImage(img, destimg, mainGame->gameConf.use_image_scale_multi_thread);
img->drop();
imageManager.tThumbLoadingMutex.lock();
if(imageManager.tThumbLoadingThreadRunning)
......
......@@ -4,6 +4,8 @@
#include "config.h"
#include "data_manager.h"
#include <unordered_map>
#include <vector>
#include <string>
#include <queue>
#include <mutex>
......@@ -11,6 +13,7 @@ namespace ygo {
class ImageManager {
private:
void resizeImage(irr::video::IImage* src, irr::video::IImage* dest, bool use_threading);
irr::video::ITexture* addTexture(const char* name, irr::video::IImage* srcimg, irr::s32 width, irr::s32 height);
// Internal implementation using void* for type erasure
......@@ -26,7 +29,6 @@ private:
};
return static_cast<RetType>(LoadFromSearchPathsImpl(code, subpath, extensions, wrapper, &callback));
}
public:
std::vector<std::wstring> ImageList[7];
int saved_image_id[7];
......
#include "image_resizer.h"
#include <cmath>
#ifdef _OPENMP
#include <omp.h>
#endif
#define STB_IMAGE_RESIZE2_IMPLEMENTATION
#include "stb_image_resize2.h"
namespace ygo {
ImageResizer imageResizer;
struct StbSamplerCache {
STBIR_RESIZE resize{};
int in_w = 0;
int in_h = 0;
int out_w = 0;
int out_h = 0;
stbir_pixel_layout layout = STBIR_BGRA;
bool samplers_built = false;
~StbSamplerCache() {
if(samplers_built) {
stbir_free_samplers(&resize);
samplers_built = false;
}
}
void reset_if_needed(int new_in_w, int new_in_h, int new_out_w, int new_out_h, stbir_pixel_layout new_layout) {
if(new_in_w == in_w && new_in_h == in_h && new_out_w == out_w && new_out_h == out_h && new_layout == layout)
return;
if(samplers_built) {
stbir_free_samplers(&resize);
samplers_built = false;
}
in_w = new_in_w;
in_h = new_in_h;
out_w = new_out_w;
out_h = new_out_h;
layout = new_layout;
resize = STBIR_RESIZE{};
}
};
/**
* Scale image using stb_image_resize2.
* Returns true on success, false on failure or unsupported format.
*/
bool ImageResizer::imageScaleSTB(irr::video::IImage* src, irr::video::IImage* dest) {
if(!src || !dest)
return false;
const auto srcDim = src->getDimension();
const auto destDim = dest->getDimension();
if(srcDim.Width == 0 || srcDim.Height == 0 || destDim.Width == 0 || destDim.Height == 0)
return false;
if(src->getColorFormat() != dest->getColorFormat())
return false;
stbir_pixel_layout layout = STBIR_BGRA;
// Fast-paths (8-bit per channel only):
// - ECF_A8R8G8B8: Irrlicht stores as BGRA in memory on little-endian.
// - ECF_R8G8B8: common for JPEGs (3 channels).
switch(src->getColorFormat()) {
case irr::video::ECF_A8R8G8B8:
layout = STBIR_BGRA;
break;
case irr::video::ECF_R8G8B8:
layout = STBIR_RGB;
break;
default:
return false;
}
void* srcPtr = src->lock();
if(!srcPtr)
return false;
void* destPtr = dest->lock();
if(!destPtr) {
src->unlock();
return false;
}
const int srcStride = (int)src->getPitch();
const int destStride = (int)dest->getPitch();
thread_local StbSamplerCache cache;
cache.reset_if_needed((int)srcDim.Width, (int)srcDim.Height, (int)destDim.Width, (int)destDim.Height, layout);
if(!cache.samplers_built) {
stbir_resize_init(&cache.resize,
srcPtr, (int)srcDim.Width, (int)srcDim.Height, srcStride,
destPtr, (int)destDim.Width, (int)destDim.Height, destStride,
layout, STBIR_TYPE_UINT8);
stbir_set_edgemodes(&cache.resize, STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP);
// Use box filters to reduce aliasing when downscaling.
stbir_set_filters(&cache.resize, STBIR_FILTER_BOX, STBIR_FILTER_BOX);
cache.samplers_built = (stbir_build_samplers(&cache.resize) != 0);
if(!cache.samplers_built) {
dest->unlock();
src->unlock();
return false;
}
} else {
// Reuse samplers but update buffer pointers for the current images
stbir_set_buffer_ptrs(&cache.resize, srcPtr, srcStride, destPtr, destStride);
}
const int ok = stbir_resize_extended(&cache.resize);
dest->unlock();
src->unlock();
return ok != 0;
}
/**
* Scale image using nearest neighbor anti-aliasing.
* Function by Warr1024, from https://github.com/minetest/minetest/issues/2419, modified.
*/
void ImageResizer::imageScaleNNAA(irr::video::IImage* src, irr::video::IImage* dest, bool use_threading) {
const auto& srcDim = src->getDimension();
const auto& destDim = dest->getDimension();
if (destDim.Width == 0 || destDim.Height == 0)
return;
// Cache scale ratios.
const double rx = (double)srcDim.Width / destDim.Width;
const double ry = (double)srcDim.Height / destDim.Height;
#pragma omp parallel if(use_threading)
{
// Walk each destination image pixel.
#pragma omp for schedule(dynamic)
for(irr::s32 dy = 0; dy < (irr::s32)destDim.Height; dy++) {
for(irr::s32 dx = 0; dx < (irr::s32)destDim.Width; dx++) {
// Calculate floating-point source rectangle bounds.
double minsx = dx * rx;
double maxsx = minsx + rx;
double minsy = dy * ry;
double maxsy = minsy + ry;
irr::u32 sx_begin = (irr::u32)std::floor(minsx);
irr::u32 sx_end = (irr::u32)std::ceil(maxsx);
if (sx_end > srcDim.Width)
sx_end = srcDim.Width;
irr::u32 sy_begin = (irr::u32)std::floor(minsy);
irr::u32 sy_end = (irr::u32)std::ceil(maxsy);
if (sy_end > srcDim.Height)
sy_end = srcDim.Height;
// Total area, and integral of r, g, b values over that area,
// initialized to zero, to be summed up in next loops.
double area = 0, ra = 0, ga = 0, ba = 0, aa = 0;
irr::video::SColor pxl, npxl;
// Loop over the integral pixel positions described by those bounds.
for(irr::u32 sy = sy_begin; sy < sy_end; sy++) {
for(irr::u32 sx = sx_begin; sx < sx_end; sx++) {
// Calculate width, height, then area of dest pixel
// that's covered by this source pixel.
double pw = 1;
if(minsx > sx)
pw += sx - minsx;
if(maxsx < (sx + 1))
pw += maxsx - sx - 1;
double ph = 1;
if(minsy > sy)
ph += sy - minsy;
if(maxsy < (sy + 1))
ph += maxsy - sy - 1;
double pa = pw * ph;
// Get source pixel and add it to totals, weighted
// by covered area and alpha.
pxl = src->getPixel(sx, sy);
area += pa;
ra += pa * pxl.getRed();
ga += pa * pxl.getGreen();
ba += pa * pxl.getBlue();
aa += pa * pxl.getAlpha();
}
}
// Set the destination image pixel to the average color.
if(area > 0) {
npxl.set((irr::u32)(aa / area + 0.5),
(irr::u32)(ra / area + 0.5),
(irr::u32)(ga / area + 0.5),
(irr::u32)(ba / area + 0.5));
} else {
npxl.set(0);
}
dest->setPixel(dx, dy, npxl);
}
}
} // end of parallel region
}
void ImageResizer::resize(irr::video::IImage* src, irr::video::IImage* dest, bool use_threading) {
if(imageScaleSTB(src, dest))
return;
imageScaleNNAA(src, dest, use_threading);
}
} // namespace ygo
#ifndef IMAGE_RESIZER_H
#define IMAGE_RESIZER_H
#include <irrlicht.h>
namespace ygo {
class ImageResizer {
private:
bool imageScaleSTB(irr::video::IImage* src, irr::video::IImage* dest);
void imageScaleNNAA(irr::video::IImage* src, irr::video::IImage* dest, bool use_threading);
public:
void resize(irr::video::IImage* src, irr::video::IImage* dest, bool use_threading);
};
extern ImageResizer imageResizer;
} // namespace ygo
#endif // IMAGE_RESIZER_H
......@@ -8,8 +8,10 @@
#ifndef _WIN32
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#include <algorithm>
#include <string>
#endif
#ifdef _WIN32
......@@ -182,7 +184,7 @@ public:
bool success = true;
TraversalDir(dir, [dir, &success](const char *name, bool isdir) {
char full_path[1024];
int len = mysnprintf(full_path, "%s/%s", dir, name);
int len = std::snprintf(full_path, sizeof(full_path), "%s/%s", dir, name);
if (len < 0 || len >= (int)(sizeof full_path)) {
success = false;
return;
......@@ -228,7 +230,7 @@ public:
while((dirp = readdir(dir)) != nullptr) {
file_unit funit;
char fname[1024];
int len = mysnprintf(fname, "%s/%s", path, dirp->d_name);
int len = std::snprintf(fname, sizeof(fname), "%s/%s", path, dirp->d_name);
if (len < 0 || len >= (int)(sizeof fname))
continue;
stat(fname, &fileStat);
......
......@@ -124,134 +124,134 @@ void SoundManager::PlaySoundEffect(int sound) {
#ifdef YGOPRO_USE_AUDIO
if(!mainGame->chkEnableSound->isChecked())
return;
char soundName[32];
std::string soundName;
switch(sound) {
case SOUND_SUMMON: {
strcpy(soundName, "summon");
soundName = "summon";
break;
}
case SOUND_SPECIAL_SUMMON: {
strcpy(soundName, "specialsummon");
soundName = "specialsummon";
break;
}
case SOUND_ACTIVATE: {
strcpy(soundName, "activate");
soundName = "activate";
break;
}
case SOUND_SET: {
strcpy(soundName, "set");
soundName = "set";
break;
}
case SOUND_FLIP: {
strcpy(soundName, "flip");
soundName = "flip";
break;
}
case SOUND_REVEAL: {
strcpy(soundName, "reveal");
soundName = "reveal";
break;
}
case SOUND_EQUIP: {
strcpy(soundName, "equip");
soundName = "equip";
break;
}
case SOUND_DESTROYED: {
strcpy(soundName, "destroyed");
soundName = "destroyed";
break;
}
case SOUND_BANISHED: {
strcpy(soundName, "banished");
soundName = "banished";
break;
}
case SOUND_TOKEN: {
strcpy(soundName, "token");
soundName = "token";
break;
}
case SOUND_NEGATE: {
strcpy(soundName, "negate");
soundName = "negate";
break;
}
case SOUND_ATTACK: {
strcpy(soundName, "attack");
soundName = "attack";
break;
}
case SOUND_DIRECT_ATTACK: {
strcpy(soundName, "directattack");
soundName = "directattack";
break;
}
case SOUND_DRAW: {
strcpy(soundName, "draw");
soundName = "draw";
break;
}
case SOUND_SHUFFLE: {
strcpy(soundName, "shuffle");
soundName = "shuffle";
break;
}
case SOUND_DAMAGE: {
strcpy(soundName, "damage");
soundName = "damage";
break;
}
case SOUND_RECOVER: {
strcpy(soundName, "gainlp");
soundName = "gainlp";
break;
}
case SOUND_COUNTER_ADD: {
strcpy(soundName, "addcounter");
soundName = "addcounter";
break;
}
case SOUND_COUNTER_REMOVE: {
strcpy(soundName, "removecounter");
soundName = "removecounter";
break;
}
case SOUND_COIN: {
strcpy(soundName, "coinflip");
soundName = "coinflip";
break;
}
case SOUND_DICE: {
strcpy(soundName, "diceroll");
soundName = "diceroll";
break;
}
case SOUND_NEXT_TURN: {
strcpy(soundName, "nextturn");
soundName = "nextturn";
break;
}
case SOUND_PHASE: {
strcpy(soundName, "phase");
soundName = "phase";
break;
}
case SOUND_MENU: {
strcpy(soundName, "menu");
soundName = "menu";
break;
}
case SOUND_BUTTON: {
strcpy(soundName, "button");
soundName = "button";
break;
}
case SOUND_INFO: {
strcpy(soundName, "info");
soundName = "info";
break;
}
case SOUND_QUESTION: {
strcpy(soundName, "question");
soundName = "question";
break;
}
case SOUND_CARD_PICK: {
strcpy(soundName, "cardpick");
soundName = "cardpick";
break;
}
case SOUND_CARD_DROP: {
strcpy(soundName, "carddrop");
soundName = "carddrop";
break;
}
case SOUND_PLAYER_ENTER: {
strcpy(soundName, "playerenter");
soundName = "playerenter";
break;
}
case SOUND_CHAT: {
strcpy(soundName, "chatmessage");
soundName = "chatmessage";
break;
}
default:
break;
return;
}
wchar_t soundNameW[32];
BufferIO::DecodeUTF8(soundName, soundNameW);
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -680,6 +680,7 @@
!counter 0x70 盘子指示物
!counter 0x71 纠罪指示物
!counter 0x1072 少女指示物
!counter 0x73 T指示物
#setnames, using tab for comment
!setname 0x1 正义盟军 AOJ
!setname 0x2 次世代 ジェネクス
......@@ -841,7 +842,7 @@
!setname 0x106e 魔导书 魔導書
!setname 0x6f 英豪 ヒロイック
!setname 0x106f 英豪挑战者 HC
#setname 0x206f 英豪冠军 H-C
!setname 0x206f 英豪冠军 HC
!setname 0x70 先史遗产 先史遺産
!setname 0x71 魔偶甜点 マドルチェ
!setname 0x72 齿轮齿轮 ギアギア
......@@ -1287,3 +1288,6 @@
!setname 0x1d8 耀圣 エルフェンノーツ
!setname 0x1d9 磁力 マグネット
!setname 0x1da 世界末日 エンド・オブ・ザ・ワールド
!setname 0x1db 妖精传姬 妖精伝姫
!setname 0x1dc 道化一座 道化の一座
!setname 0x1dd GMX
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment