开启辅助访问 切换到宽版

精易论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

用微信号发送消息登录论坛

新人指南 邀请好友注册 - 我关注人的新帖 教你赚取精币 - 每日签到


求职/招聘- 论坛接单- 开发者大厅

论坛版规 总版规 - 建议/投诉 - 应聘版主 - 精华帖总集 积分说明 - 禁言标准 - 有奖举报

查看: 16287|回复: 55
收起左侧

[技术文章] 逆向三期C语言阶段作业贪吃蛇

[复制链接]
发表于 2023-8-12 17:39:15 | 显示全部楼层 |阅读模式   河南省郑州市
本帖最后由 lilipo 于 2023-8-12 17:45 编辑

C语言学习进入尾声时,老师布置了作业,让我们用前一段时间学习的C语言知识写一些小游戏。提到小游戏,我首先想到的就是“俄罗斯方块”和“贪吃蛇”了。因为“俄罗斯方块”前几天老师刚刚带着大家写过了。所以,这一次的作业我打算写“贪吃蛇”。
对于一个大作业,我是这样做的:先分析竞品,然后构思自己的程序,再然后整理素材,再写出设计文档,最后就开始编写代码了。如图:
                              
下面,我选取其中的一部分来说明一下。
在竞品分析这一个阶段,我会从网上搜索一些文案、图片、视频。通过仔细的观查了解这个小游戏的信息,比如,有哪些元素,有怎样的用户操作,游戏的规则又是怎么的。把这些信息整理出来,再结合自己独特的想法。就可以整理出下面的图来。如图:
这次做的贪吃蛇小游戏,是想模仿诺基亚手机上贪吃蛇游戏的样子。是像素风格的。所以,素材的准备上也很简单。不论是蛇、还是食物都是由一个一个的小方块组成的。至于这些小方块如何拼成图案,把素材截图、再放大,就可以很清晰的知道啦。如图:
来到写设计文档环节,我会把写程序中会遇到的重点和难点罗列出来,写出这些地方的思路。像这次的贪吃蛇小游戏,我会列出蛇节点、食物、存档文件等的结构体;像界面菜单的绘制、界面的切换、蛇的移动、如何吃食物、如何刷新食物、如何刷新出奖励等等,也是会先写出思路来。这样在接下来写程序时,就可以方便的写出一个“外壳”和“很多的TODO”来。然后,逐个按着上面的思路来消除这些“TODO”,这样程序就被一步一步写出来了。
接下来,我将挑选部分代码来讲一下。
1、   结构体
比如,节点结构体,在程序中用节点的地方还是挺多的。比如墙(墙的某一个小方块)、蛇身(蛇身的某一小节)。所以,我采用了嵌套结构体的方式。
// 占内存大小为0x4
typedefstruct linear_node {
   // 节点指针域
   struct linear_node *pre;
   struct linear_node *next;
} LinearNode;
// 蛇节点结构体
typedefstruct {
   LinearNode base; // 用于方便操作链表
   enum snake_node_type type; // 节点类型
   enum snake_node_direction direction; // 当前节点方向
   enum snake_node_direction next_direction; // 下一步节点方向
   int x; // 节点坐标x轴值
   int y; // 节点坐标y轴值
   enum snake_node_direction elbow;
   int virtual_head;
}SnakeNode;
// 墙结构体
typedefstruct {
   LinearNode base; // 用于方便操作链表
   int x; // 节点坐标x轴值
   int y; // 节点坐标y轴值
}SnakeWall;
LinearNode结构体包含了节点的最基础的信息:“前一个节点”和“后一个节点”这两个指针。
SnakeNode结构体中的第一个成员是LinearNode结构体类型。然后才是蛇节点独有的信息。这样嵌套的结构体,就形成了类似于“父类”和“子类”的层级关系。方便在后续的代码中使用基于LinearNode结构体的链表相关算法的函数。
SnakeWall结构体也是一样。第一个成员是LinearNode结构体类型。然后才是墙节点独有的信息。
2、   链表
为了使用方便,使用双向链表。定义的结构体如下:
typedefstruct {
   LinearNode *head;
   LinearNode *rear;
   int count;
}DList;
双向链表,就是在结构体中,定义头节点指针和尾节点指针。这两个成员是必须的。有了这两个成员,就可以往链表中插入数据、查找数据、删除数据和遍历链表。第三个成员int count;是附加的信息。这些附加的信息可以让自己的链表拥有更丰富的功能。比如:可以方便的判断是链表是不是空链表;快速的返回链表中的元素个数。
下面,把链表中常用的函数也列出来:
// 双向链表初始化
voiddouble_link_list_init(DList *dlist)
{
   if (!dlist) {
     printf("数据不正确!");
     return ;
   }
   dlist->head = NULL;
   dlist->rear = NULL;
   dlist->count = 0;
}
// 释放双向链表
voiddouble_link_list_free(DList *dlist)
{
   LinearNode *head;
   LinearNode *temp;
   
   head = dlist->head;
   while (head) {
     temp = head->next;
     free(head);
     head = temp;
   }
   dlist->head = NULL;
   dlist->rear = NULL;
   dlist->count = 0;
}
// 双向链表追加数据
void* double_link_list_append(DList *dlist, size_t data_size)
{
   LinearNode *new_data;
   
   if (!dlist) {
     printf("数据不正确!");
     return  NULL;
   }
   // 分配内存
   new_data = (LinearNode *) malloc(data_size);
   if (!new_data) {
     printf("分配内存失败!");
     return NULL;
   }
   // new_data->data = val;
   new_data->pre = dlist->rear;
   new_data->next = NULL;
   if (dlist->count == 0) {
     dlist->head = dlist->rear = new_data;
   } else {
     dlist->rear->next = new_data;
     dlist->rear = new_data;
   }
   dlist->count ++;
   return new_data;
}
// 双向链表插入数据
void* double_link_list_insert(DList *dlist, int idx, size_t data_size)
{
   LinearNode *temp;
   LinearNode *new_data;
   int i = 0;
   if (idx < 0 || idx > dlist->count -1) {
     printf("插入位置错误");
     return NULL;
   }
   
   temp = dlist->head;
   while (temp && i < idx) {
     temp = temp->next;
     i++;
   }
   // 分配内存
   new_data = (LinearNode *) malloc(data_size);
   if (!new_data) {
     printf("分配内存失败!");
     return NULL;
   }
   // new_data->data = val;
   if (dlist->count == 0) {
     new_data->pre = NULL;
     new_data->next = NULL;
     dlist->head = dlist->rear = new_data;
   } else {
     new_data->pre = temp->pre;
     new_data->next = temp;
     if (temp->pre) {
        temp->pre->next = new_data;
     } else {
        dlist->head =  new_data;
     }
     temp->pre = new_data;
   }
   dlist->count ++;
   return new_data;
}
// 双向链表返回指定位置的元素
LinearNode*double_link_list_get(DList *dlist, int idx)
{
   LinearNode *temp;
   int i = 0;
   
   if (idx < 0 || idx > dlist->count -1) {
     printf("删除位置错误!");
     return NULL;
   }
   if (dlist->count == 0) {
     printf("链表是空!");
     return NULL;
   }
   
   temp = dlist->head;
   while (temp && i < idx) {
     temp = temp->next;
     i++;
   }
   return temp;
}
// 双向链表删除数据
intdouble_link_list_delete(DList *dlist, int idx)
{
   LinearNode *temp;
   int i = 0;
   
   if (idx < 0 || idx > dlist->count -1) {
     printf("删除位置错误!");
     return 0;
   }
   if (dlist->count == 0) {
     printf("链表是空,先添加数据!");
     return 0;
   }
   
   temp = dlist->head;
   while (temp && i < idx) {
     temp = temp->next;
     i++;
   }
   // *val = temp->data;
   // 分配内存
   if (temp->pre) {
     temp->pre->next = temp->next;
   } else {
     dlist->head =  temp->next;
   }
   if (temp->next) {
     dlist->rear = temp->pre;
   } else {
     temp->next->pre = temp->pre;
   }
   free(temp);
   dlist->count --;
   return 1;
}
// 双向链表返回元素的个数
intdouble_link_list_size(DList *dlist)
{
   return dlist->count;
}
// 遍历队双向链表有元素(从第一个元素到最后一个元素)
voiddouble_link_list_traversal(DList *dlist, void (*traversal_item)(int , void *))
{
   LinearNode *temp;
   int i;
   
   i = 0;
   temp = dlist->head;
   while (temp) {
     if (traversal_item) {
        traversal_item(i, temp);
     }
     i++;
     temp = temp->next;
   }
}
// 双向链表冒泡排序
voiddouble_link_list_sort_bubble(DList *dlist, int (*compare)(void *, void *), void(*swap)(void *, void *))
{
   int i;
   int j;
   LinearNode *p, *q;
   int size;
   size = double_link_list_size(dlist);
   for (i = 0; i < size-1; i++) {
     j = size - i - 1;
     p = dlist->head;
     q = p->next;
     while (j--) {
        if( compare(p, q) ) {
          swap(p, q);
        }
        p = q;
        q = q->next;
     }
   }
}
需要注意的是double_link_list_traversal函数和double_link_list_sort_bubble函数。这两个函数用到了函数指针。比如double_link_list_traversal函数,这个函数的第二个参数是一个函数指针。函数指针有两个参数,分别是当前遍历元素的下标和当前遍历元素的指针。采用函数指针的方案,是为了这些工具函数更具有通用性。“使用者”在应对不同的场景时,只需要更换函数指针即可,而不用担心“遍历”、“排序”这些主要的逻辑受到影响。
3、   界面切换
程序中会有多个界面,如:“菜单界面”、“操作说明界面”、“游戏界面”还有“关于界面”。
打开游戏,首先进入的是“菜单界面”。在“菜单界面”中,用户可以通过控制一个“选择条”的上下移动来进入不同的界面。我们可以把这个“选择条”选中的结果看作是一个数字,像1、2、3、4。然后判断具体是哪个数字就进入到哪个界面。说到这里,你是不是已经想到了解决方法了呢?是的,switch … case,用switch语句来处理在适合不过了。来看代码:
   menu = -1;
   has_save = read_game_save(&seri);
   do {
     switch(menu) {
     case 1:
        // 关于
        menu = enter_about_page();
        break;
     case 2:
        // 操作说明
        menu = enter_help_page();
        break;
     case 3:
        // 继续游戏
        menu = enter_game_page(has_save,&seri);
        break;
     case 4:
        // 新游戏
        has_save = 0;
        menu = enter_game_page(has_save,&seri);
        has_save = read_game_save(&seri);
        break;
     default:
        // 菜单
        menu = enter_menu_page(has_save);
        break;
     }
   } while(menu);
4、   绘制蛇
绘制蛇,是这个小小的游戏程序中最难的部分了。不过,不用担心,和“最难”相伴随的是“更有挑战性”、“更有趣”和“更有成就感”。接下来,我就带着大家一步一步到绘制蛇的问题解决了。
我们先把这个“大功能”细分成多个“小功能”。如:把绘制蛇,细分成“绘制蛇头”、“绘制蛇身”、“绘制蛇尾”。
其中“绘制蛇身”还可以细分为绘制正常的蛇身(直的),和绘制特殊的蛇身(弯部)。
以上提到的“蛇头”、“蛇身”、“蛇尾”,虽然有各自的名称,其实他们都有一个共同的特点:都是一个“蛇节点”。所以,不要被这众多的名称吓倒。因为,经过咱们的“抽象”,只需要写“一个函数”就可以了,就是“绘制蛇节点”。如果是普通的贪吃蛇游戏,抽象到这一步就可以了。只不过呢,我们这次做的蛇稍微形象一点:蛇头上有五官,蛇身上有花纹,蛇尾上还有小尖尖,爬行的时候腰还一扭一扭的。这就要求我们的“蛇节点”有两点新增的信息:1、独有的形状;2、方向。
形状我们可以用二维数组来定义,如:
// 右向
staticconst int shape_snake_head_1[5][5] = {
   {0, 1, 1, 0, 0},
   {1, 1, 0, 1, 1},
   {1, 1, 1, 1, 1},
   {1, 1, 0, 1, 1},
   {1, 1, 1, 0, 0},
};
需要注意的是,当我们定义这个二维数组时,我们需要标注上这个二维数组针对的哪个方向。因为,在绘制不同的方向时,我们不需要再定义另一个数组,只需要把这个数组旋转即可。
细心的朋友,会发现代码中还定义了一个相似的二维数组:
// 右向
staticconst int shape_snake_head_2[5][5] = {
   {1, 1, 1, 0, 0},
   {1, 1, 0, 1, 1},
   {1, 1, 1, 1, 1},
   {1, 1, 0, 1, 1},
   {0, 1, 1, 0, 0},
};
定义这个数组的目的是为了“摇摆起来”。就是上文提到的“爬行的时候腰还一扭一扭的”。
有了形状的定义之后,接下来,就可以写那个抽象的绘制蛇节点了。还记得我们说过有方向的事吗?所以,绘制蛇节点就有4个函数,分别对应上、下、左、右。如下(4个函数在代码中的书写顺序是上右下左,是为了好记,和css样式的顺序一致):
// 画蛇节点(向上)
staticvoid draw_snake_node_up(int base_x, int base_y, const int (*shape)[5]);
// 画蛇节点(向右)
staticvoid draw_snake_node_right(int base_x, int base_y, const int (*shape)[5]);
// 画蛇节点(向下)
staticvoid draw_snake_node_down(int base_x, int base_y, const int (*shape)[5]);
// 画蛇节点(向左)
staticvoid draw_snake_node_left(int base_x, int base_y, const int (*shape)[5]);
我们选其中一个把代码贴出来:
// 画蛇节点(向上)
staticvoid draw_snake_node_up(int base_x, int base_y, const int (*shape)[5])
{
   int i;
   int j;
   int left, top, right, bottom;
   
   for (j = 4; j >= 0; j--) {
     for (i = 0; i < 5; i++) {
        if (shape[j]) {
          left = base_x + i * MIN_PIXEL;
          top = base_y + (4-j) * MIN_PIXEL;
          right = left + MIN_PIXEL;
          bottom = top + MIN_PIXEL;
          solidrectangle(left, top, right,bottom);
        }
     }
   }
}
我们这里选了“画蛇节点(向上)”。注意看,函数中两层for循环的i、j变量,并不是i在外,j在内;初始值也不是0。这就是上面提到的应对不同方向时的“旋转”。两层for循环中有if判断,当是1时,就是需要绘制的部分。里面的MIN_PIXEL表示的是,一个“小黑点”对应电脑屏幕上的多少个像素。
有了绘制抽象“蛇节点”的函数后,绘制“蛇头”、“蛇身”、“蛇尾”这些代码就好写了。只需要提供具体的形状和具体的方向,然后调用绘制抽象“蛇节点”的函数就可以了。代码以绘制蛇头为例:
// 画蛇头
staticvoid draw_snake_head(SnakeNode *snake_node, const int (*shape)[5])
{
   switch(snake_node->direction) {
   case direction_up:
     draw_snake_node_up(snake_node->x,snake_node->y, shape);
     break;
   case direction_right:
     draw_snake_node_right(snake_node->x,snake_node->y, shape);
     break;
   case direction_down:
     draw_snake_node_down(snake_node->x,snake_node->y, shape);
     break;
   case direction_left:
     draw_snake_node_left(snake_node->x,snake_node->y, shape);
     break;
   default:
     break;
   }
}
代码写在这里,绘制“蛇头”、“蛇身”、“蛇身弯部”、“蛇尾”的函数都已经准备好了。接下来,就可以用这些函数来拼装出完整的绘制蛇的函数了。代码如下:
staticvoid draw_snake(DList *snake_node_list)
{
   int i;
   int count;
   SnakeNode *temp_snake_node;
   count =double_link_list_size(snake_node_list);
   for (i = 0; i < count; i++) {
     temp_snake_node = (SnakeNode*)double_link_list_get(snake_node_list, i);
     if (temp_snake_node->type == snake_head){
        if (move_count_odd_even % 2 == 0) {
          draw_snake_head(temp_snake_node,shape_snake_head_1);
        } else {
          draw_snake_head(temp_snake_node,shape_snake_head_2);
        }
     } else if (temp_snake_node->type ==snake_body) {
        if (temp_snake_node->next_direction== temp_snake_node->direction) {
          if (move_count_odd_even % 2 == 0) {
             if (i % 2 == 0) {
               draw_snake_body(temp_snake_node,shape_snake_body_1);
             } else {
               draw_snake_body(temp_snake_node,shape_snake_body_2);
             }
          } else {
             if (i % 2 != 0) {
               draw_snake_body(temp_snake_node,shape_snake_body_1);
             } else {
               draw_snake_body(temp_snake_node,shape_snake_body_2);
             }
          }
        } else {
          draw_snake_elbow(temp_snake_node);
        }
     } else if (temp_snake_node->type ==snake_tail) {
        if (move_count_odd_even % 2 == 0) {
          draw_snake_tail(temp_snake_node,shape_snake_tail_1);
        } else {
          draw_snake_tail(temp_snake_node,shape_snake_tail_2);
        }
     }
   }
}
可以看到,我们的这个函数的参数竟然是Dlist指针类型。是的,没有错。因为,在最初我们讲嵌套结构体时就提到了“蛇节点”结构体的第一个成员就是“普通的节点”类型。所以,用“蛇节点”组成的链表,就可以使用在链表的工具函数上。像:“count = double_link_list_size(snake_node_list);”、“temp_snake_node = (SnakeNode *)double_link_list_get(snake_node_list,i);”这些语句都是例子。现在是不是对嵌套结构体的体会更深刻了?
由于我们前面已经把绘制各部分的函数都写好了,所以这个函数中其余的代码逻辑就很简单了,就是判断“部位”,然后调用绘制对应部位的函数。再然后,就是根据“一二一、一二一”这样的口号,传入不同的奇偶形状,让我们绘制的这条小蛇活灵活现的扭动起来。
5、   存档
代码写到这里,大部分的TODO已经被我们消灭了。这款小游戏也已经可以玩起来了。为了提升游戏体验,或者为了方便给好朋友分享自己的游戏精彩时刻,再添加一个存档的功能会更好。
存档其实很简单,还记得开篇时,我分析这款小游戏中有哪些元素吗?像:蛇、墙、食物等等,还记得不?存档就是把这些元素对应的数据保存成文件。把数据写文件用到的函数有fopen和fwrite。这些元素的信息很多,比如食物,有食物的位置,还有食物消失的剩余时间;如果我们每一个元素的数据单独保存的话太多了。于是,我们可以采用定义存档结构体的方式,保存时,直接往文件中写入存档结构体的二进制数据即可以。存档结构体的定义如下:
// 存档结构体
// 魔数、游戏模式、得分数、关卡数、新方向、剩余奖励时间数、食物结构体数量、食物结构体数组、蛇结构体数量、蛇结构体数组、墙坐标数量、墙坐标数组。
structsnake_serialization {
   // 魔数
   char magic_number[32];
   // 游戏结束
   int game_over;
   // 得分数
   int score;
   // 新方向
   enum snake_node_direction new_direction;
   // 剩余奖励时间数
   int remainder_reward_second;
   // 食物结构体数量
   int food_count;
   // 蛇结构体数量
   int snake_node_count;
};
有了结构体以后,写存档的函数就简单了。代码逻辑就是:先创建结构体,然后二进制写入。上代码:
// 写游戏存档
intwrite_game_save(int game_over, int score, enum snake_node_directionnew_direction, int remainder_reward_second, SnakeFood *bean, SnakeFood *apple,DList *snake_node_list)
{
   struct snake_serialization game_save;
   FILE *file;
   int count;
   int i;
   SnakeNode *temp_snake_node;
   file = fopen(GAME_SAVE_FILE_NAME,"wb");
   if (!file) {
     return 0;
   }
   memset(game_save.magic_number, '\0', 32);
   strncpy(game_save.magic_number, "snakev1.0.0 lilipo", 32);
   game_save.game_over = game_over;
   game_save.score = score;
   game_save.new_direction = new_direction;
   game_save.remainder_reward_second =remainder_reward_second;
   game_save.food_count = 0;
   if (bean) {
     game_save.food_count++;
   }
   if (apple) {
     game_save.food_count++;
   }
   game_save.snake_node_count =double_link_list_size(snake_node_list);
   count = fwrite(&game_save, sizeof(structsnake_serialization), 1, file);
   if (count == 0) {
     fclose(file);
     return 0;
   }
   if (bean) {
     count = fwrite(bean, sizeof(SnakeFood), 1,file);
     if (count == 0) {
        fclose(file);
        return 0;
     }
   }
   if (apple) {
     count = fwrite(apple, sizeof(SnakeFood), 1,file);
     if (count == 0) {
        fclose(file);
        return 0;
     }
   }
   for (i = 0; i <game_save.snake_node_count; i++) {
     temp_snake_node = (SnakeNode*)double_link_list_get(snake_node_list, i);
     count = fwrite(temp_snake_node, sizeof(SnakeNode),1, file);
     if (count == 0) {
        fclose(file);
        return 0;
     }
   }
   fclose(file);
   return 1;
}
好了,现在代码编写完了。运行起来试玩一下,没有问题的话就可以把程序包发你的好朋友了。也把这份快乐分享出去。

完整代码:见附件


补充内容 (2023-8-16 09:23):
另外,代码中用到了 EasyX 图形库,这个库下载、安装都很方便。有需要有朋友可以留言给我。我可以发你们这个库的安装包。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

评分

参与人数 20精币 +20 收起 理由
ewake + 1 感谢分享,很给力!~
Mainli + 1 感谢分享,很给力!~
聿聿 + 1 感谢分享,很给力!~
XXGL2005 + 1 感谢分享,很给力!~
风雨3137 + 1 感谢分享,很给力!~
Zźh926 + 1 感谢分享,很给力!~
t176 + 1 感谢分享,很给力!~
3266167 + 1 感谢分享,很给力!~
望尘莫及 + 1 感谢分享,很给力!~
YzZA + 1 感谢分享,很给力!~
※逍遥游※ + 1 感谢分享,很给力!~
booms + 1 感谢分享,很给力!~
无尘666 + 1 感谢分享,很给力!~
ican8 + 1 感谢分享,很给力!~
mypursue + 1 感谢分享,很给力!~
1828902364 + 1 感谢分享,很给力!~
keyi5566 + 1 感谢分享,很给力!~
qiyuer + 1 感谢分享,很给力!~
pj小黑屋 + 1 感谢分享,很给力!~
qweipuq + 1 感谢分享,很给力!~

查看全部评分

发表于 6 天前 | 显示全部楼层   黑龙江省鸡西市
66666666666666666666
回复 支持 反对

使用道具 举报

签到天数: 5 天

发表于 2024-4-18 23:05:51 | 显示全部楼层   湖南省岳阳市
感谢分享,很给力!~
回复 支持 反对

使用道具 举报

结帖率:95% (20/21)

签到天数: 4 天

发表于 2024-4-12 10:19:18 | 显示全部楼层   安徽省宣城市
感谢分享大佬
回复 支持 反对

使用道具 举报

发表于 2024-4-11 13:42:26 | 显示全部楼层   广东省广州市
1111111111111111111111111111
回复 支持 反对

使用道具 举报

发表于 2024-4-11 10:00:43 | 显示全部楼层   湖南省邵阳市
EasyX图形库
回复 支持 反对

使用道具 举报

结帖率:50% (1/2)

签到天数: 7 天

发表于 2024-4-2 15:36:08 | 显示全部楼层   美国
回复 支持 反对

使用道具 举报

结帖率:0% (0/2)

签到天数: 22 天

发表于 2024-3-29 09:05:40 | 显示全部楼层   广西壮族自治区玉林市
谢谢分享!
回复 支持 反对

使用道具 举报

发表于 2024-3-27 16:34:25 | 显示全部楼层   湖北省襄阳市
受教了,学习了
回复 支持 反对

使用道具 举报

结帖率:0% (0/1)
发表于 2024-3-13 20:16:24 | 显示全部楼层   河北省秦皇岛市
感谢分享,很给力!~
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则 致发广告者

发布主题 收藏帖子 返回列表

sitemap| 易语言源码| 易语言教程| 易语言论坛| 诚聘英才| 易语言模块| 手机版| 广告投放| 精易论坛
拒绝任何人以任何形式在本论坛发表与中华人民共和国法律相抵触的言论,本站内容均为会员发表,并不代表精易立场!
论坛帖子内容仅用于技术交流学习和研究的目的,严禁用于非法目的,否则造成一切后果自负!如帖子内容侵害到你的权益,请联系我们!
防范网络诈骗,远离网络犯罪 违法和不良信息举报电话0663-3422125,QQ: 800073686,邮箱:800073686@b.qq.com
Powered by Discuz! X3.4 揭阳市揭东区精易科技有限公司 ( 粤ICP备12094385号-1) 粤公网安备 44522102000125 增值电信业务经营许可证 粤B2-20192173

快速回复 返回顶部 返回列表