瀏覽代碼

Properly implement program termination, including in the GTK program.

legacy
Sam Hocevar sam 15 年之前
父節點
當前提交
d5ffa9fe4f
共有 15 個檔案被更改,包括 186 行新增73 行删除
  1. +2
    -0
      src/debugsprite.cpp
  2. +3
    -3
      src/dict.cpp
  3. +0
    -10
      src/entity.cpp
  4. +2
    -6
      src/entity.h
  5. +8
    -6
      src/font.cpp
  6. +14
    -3
      src/gtk/editor.cpp
  7. +6
    -6
      src/gtk/editor.xml
  8. +23
    -9
      src/gtk/glmapview.cpp
  9. +3
    -3
      src/gtk/glmapview.h
  10. +2
    -7
      src/sdlinput.cpp
  11. +1
    -1
      src/sdlinput.h
  12. +2
    -2
      src/test-map.cpp
  13. +107
    -11
      src/ticker.cpp
  14. +5
    -0
      src/ticker.h
  15. +8
    -6
      src/tileset.cpp

+ 2
- 0
src/debugsprite.cpp 查看文件

@@ -35,6 +35,7 @@ DebugSprite::DebugSprite(Game *game)
{ {
data = new DebugSpriteData(); data = new DebugSpriteData();
data->game = game; data->game = game;
Ticker::Ref(game);
data->tiler = Tiler::Register("art/test/character-dress.png"); data->tiler = Tiler::Register("art/test/character-dress.png");
data->x = 320; data->x = 320;
data->y = 206; data->y = 206;
@@ -71,6 +72,7 @@ void DebugSprite::TickDraw(float deltams)


DebugSprite::~DebugSprite() DebugSprite::~DebugSprite()
{ {
Ticker::Unref(data->game);
Tiler::Deregister(data->tiler); Tiler::Deregister(data->tiler);
delete data; delete data;
} }


+ 3
- 3
src/dict.cpp 查看文件

@@ -85,7 +85,7 @@ int Dict::MakeSlot(char const *name)
} }
else else
{ {
data->entities[id]->Ref();
Ticker::Ref(data->entities[id]);
} }


return id; return id;
@@ -93,14 +93,14 @@ int Dict::MakeSlot(char const *name)


void Dict::RemoveSlot(int id) void Dict::RemoveSlot(int id)
{ {
if (data->entities[id]->Unref() == 0)
if (Ticker::Unref(data->entities[id]) == 0)
data->entities[id] = NULL; data->entities[id] = NULL;
} }




void Dict::SetEntity(int id, Entity *entity) void Dict::SetEntity(int id, Entity *entity)
{ {
entity->Ref();
Ticker::Ref(entity);
data->entities[id] = entity; data->entities[id] = entity;
} }




+ 0
- 10
src/entity.cpp 查看文件

@@ -63,13 +63,3 @@ void Entity::TickDraw(float deltams)
#endif #endif
} }


void Entity::Ref()
{
ref++;
}

int Entity::Unref()
{
return --ref;
}


+ 2
- 6
src/entity.h 查看文件

@@ -22,10 +22,6 @@ class Entity
friend class TickerData; friend class TickerData;
friend class Dict; friend class Dict;


public:
virtual void Ref();
virtual int Unref();

protected: protected:
typedef enum typedef enum
{ {
@@ -47,8 +43,8 @@ protected:
virtual void TickGame(float deltams); virtual void TickGame(float deltams);
virtual void TickDraw(float deltams); virtual void TickDraw(float deltams);


Entity *next;
int ref, destroy;
Entity *next, *autonext;
int ref, autorelease, destroy;


#if !FINAL_RELEASE #if !FINAL_RELEASE
enum enum


+ 8
- 6
src/font.cpp 查看文件

@@ -74,7 +74,14 @@ void Font::TickDraw(float deltams)
{ {
Entity::TickDraw(deltams); Entity::TickDraw(deltams);


if (data->img)
if (destroy)
{
if (data->img)
SDL_FreeSurface(data->img);
else
glDeleteTextures(1, &data->texture);
}
else if (data->img)
{ {
data->width = data->img->w / 16; data->width = data->img->w / 16;
data->height = data->img->h / 16; data->height = data->img->h / 16;
@@ -91,11 +98,6 @@ void Font::TickDraw(float deltams)
SDL_FreeSurface(data->img); SDL_FreeSurface(data->img);
data->img = NULL; data->img = NULL;
} }
else if (ref == 0)
{
glDeleteTextures(1, &data->texture);
destroy = 1;
}
} }


char const *Font::GetName() char const *Font::GetName()


+ 14
- 3
src/gtk/editor.cpp 查看文件

@@ -16,6 +16,15 @@
#include "glmapview.h" #include "glmapview.h"
#include "debugfps.h" #include "debugfps.h"


static gboolean delayed_quit(GtkWidget *w, GdkEvent *e, void *data)
{
(void)w;
(void)e;
(void)data;
gtk_main_quit();
return TRUE;
}

int main(int argc, char **argv) int main(int argc, char **argv)
{ {
/* Initialize GTK */ /* Initialize GTK */
@@ -41,12 +50,14 @@ int main(int argc, char **argv)
GlMapView *glmapview = new GlMapView(builder); GlMapView *glmapview = new GlMapView(builder);


/* Show window. We're good to go! */ /* Show window. We're good to go! */
gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(builder, "window")));
GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "window"));
gtk_widget_show_all(window);
gtk_signal_connect(GTK_OBJECT(window), "delete_event",
GTK_SIGNAL_FUNC(delayed_quit), NULL);
g_object_unref(G_OBJECT(builder)); g_object_unref(G_OBJECT(builder));


new DebugFps();
glmapview->LoadMap("maps/testmap.tmx"); glmapview->LoadMap("maps/testmap.tmx");
glmapview->SetFocus();
new DebugFps();


gtk_main(); gtk_main();




+ 6
- 6
src/gtk/editor.xml 查看文件

@@ -4,7 +4,6 @@
<!-- interface-naming-policy project-wide --> <!-- interface-naming-policy project-wide -->
<object class="GtkWindow" id="window"> <object class="GtkWindow" id="window">
<property name="title" translatable="yes">Deus Hax Editor</property> <property name="title" translatable="yes">Deus Hax Editor</property>
<signal name="delete_event" handler="gtk_main_quit"/>
<child> <child>
<object class="GtkVBox" id="vbox1"> <object class="GtkVBox" id="vbox1">
<property name="visible">True</property> <property name="visible">True</property>
@@ -23,8 +22,8 @@
<child> <child>
<object class="GtkImageMenuItem" id="imagemenuitem1"> <object class="GtkImageMenuItem" id="imagemenuitem1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="related_action">action_new</property>
<property name="use_action_appearance">True</property> <property name="use_action_appearance">True</property>
<property name="related_action">action_new</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
</object> </object>
@@ -32,8 +31,8 @@
<child> <child>
<object class="GtkImageMenuItem" id="imagemenuitem2"> <object class="GtkImageMenuItem" id="imagemenuitem2">
<property name="visible">True</property> <property name="visible">True</property>
<property name="related_action">action_open</property>
<property name="use_action_appearance">True</property> <property name="use_action_appearance">True</property>
<property name="related_action">action_open</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="use_stock">True</property> <property name="use_stock">True</property>
</object> </object>
@@ -67,6 +66,7 @@
<property name="use_stock">True</property> <property name="use_stock">True</property>
<property name="always_show_image">True</property> <property name="always_show_image">True</property>
<signal name="activate" handler="gtk_main_quit"/> <signal name="activate" handler="gtk_main_quit"/>
<signal name="activate" handler="gtk_true"/>
</object> </object>
</child> </child>
</object> </object>
@@ -158,8 +158,8 @@
<child> <child>
<object class="GtkToolButton" id="toolbutton2"> <object class="GtkToolButton" id="toolbutton2">
<property name="visible">True</property> <property name="visible">True</property>
<property name="related_action">action_new</property>
<property name="use_action_appearance">True</property> <property name="use_action_appearance">True</property>
<property name="related_action">action_new</property>
<property name="label" translatable="yes">toolbutton</property> <property name="label" translatable="yes">toolbutton</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
</object> </object>
@@ -171,8 +171,8 @@
<child> <child>
<object class="GtkToolButton" id="toolbutton1"> <object class="GtkToolButton" id="toolbutton1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="related_action">action_open</property>
<property name="use_action_appearance">True</property> <property name="use_action_appearance">True</property>
<property name="related_action">action_open</property>
<property name="label" translatable="yes">Open...</property> <property name="label" translatable="yes">Open...</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
</object> </object>
@@ -184,8 +184,8 @@
<child> <child>
<object class="GtkToolButton" id="toolbutton4"> <object class="GtkToolButton" id="toolbutton4">
<property name="visible">True</property> <property name="visible">True</property>
<property name="related_action">action_save</property>
<property name="use_action_appearance">True</property> <property name="use_action_appearance">True</property>
<property name="related_action">action_save</property>
<property name="label" translatable="yes">Save</property> <property name="label" translatable="yes">Save</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
</object> </object>


+ 23
- 9
src/gtk/glmapview.cpp 查看文件

@@ -50,11 +50,10 @@ GlMapView::GlMapView(GtkBuilder *builder)
* stealing time from the GTK loop when the callback time exceeds * stealing time from the GTK loop when the callback time exceeds
* the timeout value. */ * the timeout value. */
g_idle_add((GSourceFunc)IdleTickSignal, this); g_idle_add((GSourceFunc)IdleTickSignal, this);
gtk_quit_add(0, (GtkFunction)ShutdownSignal, this);


gtk_signal_connect(GTK_OBJECT(glarea), "realize", gtk_signal_connect(GTK_OBJECT(glarea), "realize",
GTK_SIGNAL_FUNC(SetupSignal), this); GTK_SIGNAL_FUNC(SetupSignal), this);
gtk_signal_connect(GTK_OBJECT(glarea), "destroy",
GTK_SIGNAL_FUNC(DestroySignal), this);
gtk_signal_connect(GTK_OBJECT(glarea), "expose_event", gtk_signal_connect(GTK_OBJECT(glarea), "expose_event",
GTK_SIGNAL_FUNC(DrawSignal), this); GTK_SIGNAL_FUNC(DrawSignal), this);
gtk_signal_connect(GTK_OBJECT(glarea), "configure_event", gtk_signal_connect(GTK_OBJECT(glarea), "configure_event",
@@ -75,17 +74,28 @@ void GlMapView::LoadMap(char const *path)
{ {
// FIXME: detect when the map viewer is killed // FIXME: detect when the map viewer is killed
mapviewer = new MapViewer(path); mapviewer = new MapViewer(path);
Ticker::Ref(mapviewer);


UpdateAdjustments(); UpdateAdjustments();
} }


void GlMapView::SetFocus()
void GlMapView::CloseMap()
{ {
gtk_widget_grab_focus(glarea);
if (mapviewer)
Ticker::Unref(mapviewer);
mapviewer = NULL;

UpdateAdjustments();
} }


gboolean GlMapView::IdleTick() gboolean GlMapView::IdleTick()
{ {
if (Ticker::Finished())
{
gtk_main_quit();
return FALSE;
}

// FIXME: do not do anything if the previous tick was too recent? // FIXME: do not do anything if the previous tick was too recent?
ticking = TRUE; ticking = TRUE;


@@ -104,6 +114,7 @@ gboolean GlMapView::IdleTick()
gboolean GlMapView::Setup() gboolean GlMapView::Setup()
{ {
/* Set up display */ /* Set up display */
gtk_widget_grab_focus(glarea);
if (gtk_gl_area_make_current(GTK_GL_AREA(glarea))) if (gtk_gl_area_make_current(GTK_GL_AREA(glarea)))
Video::Setup(glarea->allocation.width, glarea->allocation.height); Video::Setup(glarea->allocation.width, glarea->allocation.height);


@@ -112,9 +123,13 @@ gboolean GlMapView::Setup()
return TRUE; return TRUE;
} }


gboolean GlMapView::Destroy()
gboolean GlMapView::Shutdown()
{ {
g_idle_remove_by_data(this);
CloseMap();
Ticker::Shutdown();
/* Hijack the exit process by adding another level of gtk_main */
gtk_widget_set_sensitive(gtk_widget_get_toplevel(glarea), FALSE);
gtk_main();
return TRUE; return TRUE;
} }


@@ -239,10 +254,9 @@ gboolean GlMapView::SetupSignal(GtkWidget *w, GlMapView *that)
return that->Setup(); return that->Setup();
} }


gboolean GlMapView::DestroySignal(GtkWidget *w, GlMapView *that)
gboolean GlMapView::ShutdownSignal(GlMapView *that)
{ {
(void)w;
return that->Destroy();
return that->Shutdown();
} }


gboolean GlMapView::DrawSignal(GtkWidget *w, GdkEventExpose *e, gboolean GlMapView::DrawSignal(GtkWidget *w, GdkEventExpose *e,


+ 3
- 3
src/gtk/glmapview.h 查看文件

@@ -13,13 +13,13 @@ class GlMapView
public: public:
GlMapView(GtkBuilder *builder); GlMapView(GtkBuilder *builder);
void LoadMap(char const *path); void LoadMap(char const *path);
void SetFocus();
void CloseMap();


private: private:
/* Private methods */ /* Private methods */
gboolean IdleTick(); gboolean IdleTick();
gboolean Setup(); gboolean Setup();
gboolean Destroy();
gboolean Shutdown();
gboolean Draw(GdkEventExpose *e); gboolean Draw(GdkEventExpose *e);
void Scroll(double dx, double dy); void Scroll(double dx, double dy);
void UpdateAdjustments(); void UpdateAdjustments();
@@ -30,7 +30,7 @@ private:
/* Private signal slots */ /* Private signal slots */
static gboolean IdleTickSignal(GlMapView *that); static gboolean IdleTickSignal(GlMapView *that);
static gboolean SetupSignal(GtkWidget *w, GlMapView *that); static gboolean SetupSignal(GtkWidget *w, GlMapView *that);
static gboolean DestroySignal(GtkWidget *w, GlMapView *that);
static gboolean ShutdownSignal(GlMapView *that);
static gboolean DrawSignal(GtkWidget *w, GdkEventExpose *e, static gboolean DrawSignal(GtkWidget *w, GdkEventExpose *e,
GlMapView *that); GlMapView *that);
static gboolean ReshapeSignal(GtkWidget *w, GdkEventConfigure *e, static gboolean ReshapeSignal(GtkWidget *w, GdkEventConfigure *e,


+ 2
- 7
src/sdlinput.cpp 查看文件

@@ -21,7 +21,6 @@ class SdlInputData
friend class SdlInput; friend class SdlInput;


private: private:
Game *game;
int mx, my; int mx, my;
}; };


@@ -29,12 +28,11 @@ private:
* Public SdlInput class * Public SdlInput class
*/ */


SdlInput::SdlInput(Game *game)
SdlInput::SdlInput()
{ {
SDL_Init(SDL_INIT_TIMER); SDL_Init(SDL_INIT_TIMER);


data = new SdlInputData(); data = new SdlInputData();
data->game = game;
SDL_GetMouseState(&data->mx, &data->my); SDL_GetMouseState(&data->mx, &data->my);
} }


@@ -47,9 +45,6 @@ void SdlInput::TickGame(float deltams)
{ {
Entity::TickGame(deltams); Entity::TickGame(deltams);


if (data->game->Finished())
destroy = 1;

/* Handle mouse input */ /* Handle mouse input */
SDL_GetMouseState(&data->mx, &data->my); SDL_GetMouseState(&data->mx, &data->my);


@@ -58,7 +53,7 @@ void SdlInput::TickGame(float deltams)
while (SDL_PollEvent(&event)) while (SDL_PollEvent(&event))
{ {
if (event.type == SDL_QUIT) if (event.type == SDL_QUIT)
data->game->Quit();
Ticker::Shutdown();
#if 0 #if 0
else if (event.type == SDL_KEYDOWN) else if (event.type == SDL_KEYDOWN)
Input::KeyPressed(event.key.keysym.sym, deltams); Input::KeyPressed(event.key.keysym.sym, deltams);


+ 1
- 1
src/sdlinput.h 查看文件

@@ -19,7 +19,7 @@ class SdlInputData;
class SdlInput : public Entity class SdlInput : public Entity
{ {
public: public:
SdlInput(Game *game);
SdlInput();
virtual ~SdlInput(); virtual ~SdlInput();


protected: protected:


+ 2
- 2
src/test-map.cpp 查看文件

@@ -48,12 +48,12 @@ int main(int argc, char **argv)
Game *game = new Game("maps/testmap.tmx"); Game *game = new Game("maps/testmap.tmx");


/* Register an input driver and some debug stuff */ /* Register an input driver and some debug stuff */
new SdlInput(game);
new SdlInput();
new DebugFps(); new DebugFps();
new DebugSprite(game); new DebugSprite(game);
//new DebugRecord("lolengine.ogg"); //new DebugRecord("lolengine.ogg");


while (!game->Finished())
while (!Ticker::Finished())
{ {
/* Tick the game */ /* Tick the game */
Ticker::TickGame(); Ticker::TickGame();


+ 107
- 11
src/ticker.cpp 查看文件

@@ -23,7 +23,8 @@ static class TickerData


public: public:
TickerData() : TickerData() :
todo(0), nentities(0),
todolist(0), autolist(0),
nentities(0),
frame(0), deltams(0), bias(0) frame(0), deltams(0), bias(0)
{ {
for (int i = 0; i < Entity::GROUP_COUNT; i++) for (int i = 0; i < Entity::GROUP_COUNT; i++)
@@ -35,17 +36,26 @@ public:
#if !FINAL_RELEASE #if !FINAL_RELEASE
if (nentities) if (nentities)
fprintf(stderr, "ERROR: still %i entities in ticker\n", nentities); fprintf(stderr, "ERROR: still %i entities in ticker\n", nentities);
if (autolist)
{
int count = 0;
for (Entity *e = autolist; e; e = e->autonext, count++)
;
fprintf(stderr, "ERROR: still %i autoreleased entities\n", count);
}
fprintf(stderr, "INFO: %i frames required to quit\n",
frame - quitframe);
#endif #endif
} }


private: private:
/* Entity management */ /* Entity management */
Entity *todo;
Entity *todolist, *autolist;
Entity *list[Entity::GROUP_COUNT]; Entity *list[Entity::GROUP_COUNT];
int nentities; int nentities;


/* Fixed framerate management */ /* Fixed framerate management */
int frame;
int frame, quitframe;
Timer timer; Timer timer;
float deltams, bias; float deltams, bias;
} }
@@ -62,8 +72,55 @@ void Ticker::Register(Entity *entity)
/* If we are called from its constructor, the object's vtable is not /* If we are called from its constructor, the object's vtable is not
* ready yet, so we do not know which group this entity belongs to. Wait * ready yet, so we do not know which group this entity belongs to. Wait
* until the first tick. */ * until the first tick. */
entity->next = data->todo;
data->todo = entity;
entity->next = data->todolist;
data->todolist = entity;
/* Objects are autoreleased by default. Put them in a circular list. */
entity->autorelease = 1;
entity->autonext = data->autolist;
data->autolist = entity;
entity->ref = 1;

data->nentities++;
}

void Ticker::Ref(Entity *entity)
{
#if !FINAL_RELEASE
if (entity->destroy)
fprintf(stderr, "ERROR: refing entity scheduled for destruction\n");
#endif
if (entity->autorelease)
{
/* Get the entity out of the autorelease list. This is usually
* very fast since the first entry in autolist is the last
* registered entity. */
for (Entity *e = data->autolist, *prev = NULL; e;
prev = e, e = e->autonext)
{
if (e == entity)
{
if (prev)
prev->autonext = e->autonext;
else
data->autolist = e->autonext;
break;
}
}
entity->autorelease = 0;
}
else
entity->ref++;
}

int Ticker::Unref(Entity *entity)
{
#if !FINAL_RELEASE
if (entity->ref <= 0)
fprintf(stderr, "ERROR: dereferencing unreferenced entity\n");
if (entity->autorelease)
fprintf(stderr, "ERROR: dereferencing autoreleased entity\n");
#endif
return --entity->ref;
} }


void Ticker::TickGame() void Ticker::TickGame()
@@ -73,6 +130,17 @@ void Ticker::TickGame()


Profiler::Start(Profiler::STAT_TICK_GAME); Profiler::Start(Profiler::STAT_TICK_GAME);


#if 0
fprintf(stderr, "-------------------------------------\n");
for (int i = 0; i < Entity::GROUP_COUNT; i++)
{
fprintf(stderr, "Group %i\n", i);

for (Entity *e = data->list[i]; e; e = e->next)
fprintf(stderr, " \\-- %s (ref %i, destroy %i)\n", e->GetName(), e->ref, e->destroy);
}
#endif

data->frame++; data->frame++;


data->deltams = data->timer.GetMs(); data->deltams = data->timer.GetMs();
@@ -82,7 +150,8 @@ void Ticker::TickGame()
* before inserting awaiting objects, because there is no way these * before inserting awaiting objects, because there is no way these
* are already marked for destruction. */ * are already marked for destruction. */
for (int i = 0; i < Entity::GROUP_COUNT; i++) for (int i = 0; i < Entity::GROUP_COUNT; i++)
for (Entity *e = data->list[i], *prev = NULL; e; prev = e, e = e->next)
for (Entity *e = data->list[i], *prev = NULL; e; )
{
if (e->destroy) if (e->destroy)
{ {
if (prev) if (prev)
@@ -90,20 +159,30 @@ void Ticker::TickGame()
else else
data->list[i] = e->next; data->list[i] = e->next;


Entity *tmp = e;
e = e->next;
delete tmp;

data->nentities--; data->nentities--;
delete e;
} }
else
{
if (e->ref <= 0)
e->destroy = 1;
prev = e;
e = e->next;
}
}


/* Insert waiting objects into the appropriate lists */ /* Insert waiting objects into the appropriate lists */
while (data->todo)
while (data->todolist)
{ {
Entity *e = data->todo;
data->todo = e->next;
Entity *e = data->todolist;
data->todolist = e->next;


int i = e->GetGroup(); int i = e->GetGroup();
e->next = data->list[i]; e->next = data->list[i];
data->list[i] = e; data->list[i] = e;
data->nentities++;
} }


/* Tick objects for the game loop */ /* Tick objects for the game loop */
@@ -169,3 +248,20 @@ int Ticker::GetFrameNum()
return data->frame; return data->frame;
} }


void Ticker::Shutdown()
{
/* We're bailing out. Release all autorelease objects. */
while (data->autolist)
{
data->autolist->ref--;
data->autolist = data->autolist->autonext;
}

data->quitframe = data->frame;
}

int Ticker::Finished()
{
return !data->nentities;
}


+ 5
- 0
src/ticker.h 查看文件

@@ -20,11 +20,16 @@ class Ticker
{ {
public: public:
static void Register(Entity *entity); static void Register(Entity *entity);
static void Ref(Entity *entity);
static int Unref(Entity *entity);


static void TickGame(); static void TickGame();
static void TickDraw(); static void TickDraw();
static void ClampFps(float deltams); static void ClampFps(float deltams);
static int GetFrameNum(); static int GetFrameNum();

static void Shutdown();
static int Finished();
}; };


#endif // __DH_TICKER_H__ #endif // __DH_TICKER_H__


+ 8
- 6
src/tileset.cpp 查看文件

@@ -83,7 +83,14 @@ void TileSet::TickDraw(float deltams)
{ {
Entity::TickDraw(deltams); Entity::TickDraw(deltams);


if (data->img)
if (destroy)
{
if (data->img)
SDL_FreeSurface(data->img);
else
glDeleteTextures(1, &data->texture);
}
else if (data->img)
{ {
glGenTextures(1, &data->texture); glGenTextures(1, &data->texture);
glBindTexture(GL_TEXTURE_2D, data->texture); glBindTexture(GL_TEXTURE_2D, data->texture);
@@ -97,11 +104,6 @@ void TileSet::TickDraw(float deltams)
SDL_FreeSurface(data->img); SDL_FreeSurface(data->img);
data->img = NULL; data->img = NULL;
} }
else if (ref == 0)
{
glDeleteTextures(1, &data->texture);
destroy = 1;
}
} }


char const *TileSet::GetName() char const *TileSet::GetName()


Loading…
取消
儲存