开启辅助访问      

站内搜索

搜索
热搜: 下载 1.19 1.20

Minecraft(我的世界)苦力怕论坛

[软件开发讨论] 以目的为驱动的开发实例

发表于 2023-9-20 17:11:23 | 显示全部楼层 |阅读模式 IP:吉林省
本帖最后由 Shizuku- 于 2023-9-20 17:15 编辑

- 本文和Minecraft毫无关联。
- 本文涉及的语言为C。
- 项目地址:https://github.com/wxkj123/osu-utils,GPL V3 Licence

本文将详细叙述开发过程。

首先不要被结构如此复杂的源码吓到,其实本身是单文件,但是因为有些复杂,以及个人习惯问题导致单文件观感不佳,所以拆分成了这个样子。

然后安利一下这个游戏,叫osu!,没错是个音游。它是由社区驱动的,因此谱面文件都是由玩家制作并上传的。音乐嘛,有个很闹心的事情,就是会经常性地产生各种版权问题,osu!这里也不例外,那么问题就来了,打音游得听喜欢的歌,但是喜欢的歌往往又找不到,于是乎进入死循环,我根本不知道什么歌好听,找歌范围又过于宽泛,毫无目的性。

于是想了点办法,诶,别人打过的歌……应该……不会太难听吧,那可以照着别人的Best Play抄嘛(Best Play是直接挂在个人主页的)。

于是……就有了本项目。

这个程序是用来扒别人的Best Play的,当然也可以扒自己的。

想要扒这个东西,那就得要找到定位的抓手,即如何找到想要的谱面,这是其一;用什么下载,这是其二;用什么库,这是其三。解决好这几个问题,这个程序也就写出来了。

定位问题,随便打开一个谱面的info页,留心观察就会发现:

Screenshot 2023-09-20 at 15.56.24 copy.png

找到一个谱面需要有这两个数字,前者称作sid(即BeatmapSet ID),后者称作bid(Beatmap ID)。

那么获取这个数字就成了首要问题了。

既然音游群有机器人,曾经也问过某开发者适配问题,他是这么答的:

Screenshot 2023-09-20 at 16.03.06.png

哦,有API啊,那好说,直接看文档就是了。

Screenshot 2023-09-20 at 16.12.45.png

看来还有OAuth的问题。

Screenshot 2023-09-20 at 16.15.03.png

挺好的,没什么难度。

因此批量获取谱面的方法是:先向API请求token,然后抓着这个token以及uid去问API要bplist,要到了之后,就可以从返回的json里面找到sid,然后下载。

选库方面,本来打算使用libuv+cJSON,后来发现我经验不够,uv根本不会,于是改用了libcurl+cJSON练手(其实也可以找借口说uv太重了)。

于是可以开始写了。

首先需要拿到token,那么这就是目标。首先请求完会拿到一个JSON文件,然后使用cJSON即可将token取出。

代码如下:

inc/util/OAuth.h:

  1. #ifndef OAuth_h
  2. #define OAuth_h

  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <time.h>
  7. #include <curl/curl.h>
  8. #include <sys/types.h>
  9. #include <sys/stat.h>
  10. #include <unistd.h>
  11. #include "cJSON.h"

  12. void read_token(char* tokenstring);
  13. void on_token_expire(void);
  14. void get_token(int clientid,const char* clientsec);
  15. size_t token_cb(char* buffer,size_t size,size_t nitems,void* userdata);

  16. #endif /* OAuth_h */
复制代码


src/core/auth/OAuth.c:

  1. #include "OAuth.h"

  2. void read_token(char* tokenstring){
  3.     FILE* fp=fopen("Cache/token.json","r");
  4.     int failcount=0;
  5.     if(fp){
  6.         fseek(fp,0,SEEK_END);
  7.         long fsize=ftell(fp);
  8.         if(fsize==0){
  9.             on_token_expire();
  10.             exit(-1);
  11.         }
  12.         fseek(fp,0,SEEK_SET);
  13.         char ch;
  14.         char* string=(char*) malloc(fsize);
  15.         char* ptr=string;
  16.         while(!feof(fp)){
  17.             ch=fgetc(fp);
  18.             * ptr++=ch;
  19.         }
  20.         fclose(fp);
  21.         cJSON* root=cJSON_Parse(string);
  22.         free(string);
  23.         cJSON* timethen=cJSON_GetObjectItem(root,"time");
  24.         time_t previous_time=timethen->valueint;
  25.         time_t current_time=time(NULL);
  26.         if(current_time-previous_time>=86400){
  27.             on_token_expire();
  28.             exit(-2);
  29.         }
  30.         cJSON* token=cJSON_GetObjectItem(root,"token");
  31.         sprintf(tokenstring,"%s",token->valuestring);
  32.         cJSON_Delete(root);
  33.     }
  34.     else{
  35.         if(failcount>1){
  36.             fprintf(stderr,"Failed to read and get token.\n");
  37.         }
  38.         on_token_expire();
  39.         read_token(tokenstring);
  40.     }
  41. }

  42. void on_token_expire(void){
  43.     fprintf(stderr,"Failed to read token, will get another.\n");
  44.     int clientid=5;
  45.     char* clientsec="FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
  46.     get_token(clientid,clientsec);
  47. }

  48. void get_token(int clientid,const char* clientsec){
  49.     CURL* eh=curl_easy_init();
  50.     if(eh){
  51.         struct curl_slist* list;
  52.         char data[114];
  53.         sprintf(data,"client_id=%d&client_secret=%s&grant_type=client_credentials&scope=public",clientid,clientsec);
  54.         list=curl_slist_append(NULL,"Accept: application/json");
  55.         list=curl_slist_append(list,"Content-Type: application/x-www-form-urlencoded");
  56.         curl_easy_setopt(eh,CURLOPT_URL,"https://osu.ppy.sh/oauth/token");
  57.         curl_easy_setopt(eh,CURLOPT_PROXY,"http://localhost:7890");
  58.         curl_easy_setopt(eh,CURLOPT_HTTPHEADER,list);
  59.         curl_easy_setopt(eh,CURLOPT_POSTFIELDSIZE,(long)strlen(data));
  60.         curl_easy_setopt(eh,CURLOPT_POSTFIELDS,data);
  61.         curl_easy_setopt(eh,CURLOPT_TIMEOUT,30);
  62.         curl_easy_setopt(eh,CURLOPT_WRITEFUNCTION,token_cb);
  63.         curl_easy_perform(eh);
  64.         curl_easy_cleanup(eh);
  65.     }
  66.     else{
  67.         fprintf(stderr,"Failed to get token.\n");
  68.         exit(1);
  69.     }
  70. }

  71. size_t token_cb(char* buffer,size_t size,size_t nitems,void* userdata){
  72.     FILE* read=(FILE*) userdata;
  73.     size_t retcode=fread(buffer,size,nitems,read);
  74.     cJSON* root=cJSON_Parse(buffer);
  75.     cJSON* item=cJSON_GetObjectItem(root,"access_token");
  76.     char* token=item->valuestring;
  77.     time_t current=time(NULL);
  78.     cJSON* data=cJSON_CreateObject();
  79.     cJSON_AddNumberToObject(data,"time",current);
  80.     cJSON_AddStringToObject(data,"token",token);
  81.     char* jsondata=cJSON_Print(data);
  82.     FILE* fp;
  83.     fp=fopen("Cache/token.json","w+");
  84.     fprintf(fp,"%s",jsondata);
  85.     fclose(fp);
  86.     cJSON_Delete(root);
  87.     return retcode;
  88. }
复制代码


oauth这个模块只做了一件事情,就是判断是否有token文件存在,如果有就读取,没有就再来一个。

token本身是有时效性的,如果token过期了,那就得重新请求,否则可以尽情复用,所以需要文件存储。

cJSON只是一个工具,C本身不具备解析JSON的功能,但是有了这个库后就有了。

请求完token就可以请求bplist,默认已知uid,查询范围。

代码如下:

inc/util/Getbplist.h:

  1. #ifndef Getbplist
  2. #define Getbplist

  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <curl/curl.h>
  7. #include "cJSON.h"

  8. #include "util/Basic.h"
  9. #include "Mode.h"

  10. void get_bplist(int uid,uint8_t mode_id,int offset,int limit,const char* token);
  11. int* getsids(void);

  12. #endif /* Getbplist_h */
复制代码


被Getbplist.h引用的inc/util/Basic.h:

  1. #ifndef Basic_h
  2. #define Basic_h

  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>

  6. struct memory{
  7.     char* response;
  8.     size_t size;
  9. };

  10. size_t cb(char* buffer,size_t size,size_t nitems,void* userdata);

  11. #endif /* Basic_h */
复制代码

  1. inc/util/Mode.h:

  2. #ifndef Mode_h
  3. #define Mode_h

  4. #include <stdio.h>
  5. #include <stdlib.h>

  6. char* select_mode(uint8_t mode_id);

  7. #endif /* Mode_h */
复制代码


src/core/network/Getbplist.c:

  1. #include "Getbplist.h"

  2. void get_bplist(int uid,uint8_t mode_id,int offset,int limit,const char* token){
  3.     CURL* eh=curl_easy_init();
  4.     if(eh){
  5.         char* mode=select_mode(mode_id);
  6.         char url[114];
  7.         char oauth[1050];
  8.         struct curl_slist* list;
  9.         struct memory chunk={0};
  10.         sprintf(url,"https://osu.ppy.sh/api/v2/users/%d/scores/best?include_fails=0&mode=%s&limit=%d&offset=%d",uid,mode,limit,offset);
  11.         sprintf(oauth,"Authorization: Bearer %s",token);
  12.         list=curl_slist_append(NULL,"Content-Type: application/json");
  13.         list=curl_slist_append(list,"Accept: application/json");
  14.         list=curl_slist_append(list,oauth);
  15.         curl_easy_setopt(eh,CURLOPT_URL,url);
  16.         curl_easy_setopt(eh,CURLOPT_HTTPHEADER,list);
  17.         curl_easy_setopt(eh,CURLOPT_WRITEFUNCTION,cb);
  18.         curl_easy_setopt(eh,CURLOPT_WRITEDATA,(void*) &chunk);
  19.         curl_easy_setopt(eh,CURLOPT_TIMEOUT,30);
  20.         curl_easy_perform(eh);
  21.         
  22.         // Write bplist to cache.
  23.         
  24.         FILE* fp;
  25.         fp=fopen("Cache/bplist.json","w+");
  26.         fprintf(fp,"%s",chunk.response);
  27.         fclose(fp);
  28.         
  29.         free(chunk.response);
  30.         curl_easy_cleanup(eh);
  31.     }
  32.     else{
  33.         fprintf(stderr,"Failed to get bplist.\n");
  34.     }
  35. }

  36. int* getsids(void){
  37.     FILE* fp;
  38.     fp=fopen("Cache/bplist.json","r");
  39.     if(fp){
  40.         fseek(fp,0,SEEK_END);
  41.         long fsize=ftell(fp);
  42.         if(fsize==0){
  43.             exit(-1);
  44.         }
  45.         fseek(fp,0,SEEK_SET);
  46.         char ch;
  47.         char* string=malloc(fsize);
  48.         char* ptr=string;
  49.         while(!feof(fp)){
  50.             ch=fgetc(fp);
  51.             * ptr++=ch;
  52.         }
  53.         fclose(fp);
  54.         cJSON* root=cJSON_Parse(string);
  55.         int arraysize=cJSON_GetArraySize(root);
  56.         cJSON* info;
  57.         cJSON* beatmap;
  58.         cJSON* siditem;
  59.         int sid;
  60.         int* array=(int*) malloc(arraysize*sizeof(int));
  61.         for(int i=0;i<arraysize;i++){
  62.             info=cJSON_GetArrayItem(root,i);
  63.             beatmap=cJSON_GetObjectItem(info,"beatmap");
  64.             siditem=cJSON_GetObjectItem(beatmap,"beatmapset_id");
  65.             sid=siditem->valueint;
  66.             array[i]=sid;
  67.         }
  68.         cJSON_Delete(root);
  69.         return array;
  70.     }
  71.     else{
  72.         fprintf(stderr,"Failed to get sids.\n");
  73.         exit(-7);
  74.     }
  75. }
复制代码


src/core/network/Basic.c:

  1. #include "Basic.h"

  2. size_t cb(char* buffer,size_t size,size_t nitems,void* userdata){
  3.     size_t realsize=size*nitems;
  4.     struct memory *mem=(struct memory*) userdata;
  5.     char* ptr=realloc(mem->response,mem->size+realsize+1);
  6.     if(buffer==NULL){
  7.         fprintf(stderr,"Out of memory.\n");
  8.         exit(2);
  9.     }
  10.     mem->response=ptr;
  11.     memcpy(&(mem->response[mem->size]),buffer,realsize);
  12.     mem->size+=realsize;
  13.     mem->response[mem->size]=0;
  14.     return realsize;
  15. }
复制代码


src/core/op/Mode.c:

  1. #include "Mode.h"

  2. char* select_mode(uint8_t mode_id){
  3.     char* mode;
  4.     switch(mode_id){
  5.         case 0:
  6.             mode="osu";
  7.             break;
  8.         case 1:
  9.             mode="taiko";
  10.             break;
  11.         case 2:
  12.             mode="fruits";
  13.             break;
  14.         case 3:
  15.             mode="mania";
  16.             break;
  17.         default:
  18.             fprintf(stderr,"Mode error.\n");
  19.             exit(-4);
  20.             break;
  21.     }
  22.     return mode;
  23. }
复制代码


这里就是一个要注意的地方,因为libcurl的回调是基于TCP的,一次性可写不完,哪怕你说1MB不大吧,嗯,确实不大,但是就是不能一次写完。

事实上一次只能写1KB左右,那么就只能拿个容器接,什么容器呢,就是引入的Basic.h里面的那个memory结构,它就是容器。

回调执行完了之后,继续往下会发现一个事情,就是需要手工free这个chunk,即内存块,因为文件是一整块的,肯定不是什么分散的,所以很明显在free之前这个内存块都是可访问的,而且不需要在回调里面写向什么什么文件写什么数据,这是毫无必要的,因为在回调中写数据,执行一次回调就要写一次数据,那么比如说写1MB的数据,硬盘就被写了1000次,这硬盘可吃不消。那么接下来在free之前直接先把它catch住,直接朝文件里写就是了。

至于为什么要写Mode这个函数,因为0,1,2,3输入起来要比英文单词快,而且方便。

接下来最后一块就是要输出了,下载源方面,官方指定是靠不住了,为什么这么讲,因为这个csm是要靠JS去实现的,我觉得没什么必要,就麻烦一下国内的镜子吧:D。

本来下载是想要写成异步的,即所谓的多线程,但是后来一想,撑死了也就100个文件,慢又能慢到哪里去,于是就直接一个for循环结束了。

代码如下:

inc/util/Downloader.h:

  1. #ifndef Downloader_h
  2. #define Downloader_h

  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <time.h>
  7. #include <curl/curl.h>
  8. #include <sys/types.h>
  9. #include <sys/stat.h>
  10. #include <unistd.h>

  11. #include "util/Basic.h"

  12. int compare(const void* p1,const void* p2);
  13. void mapdownloader(int* sids,int offset,int limit);

  14. #endif /* Downloader_h */
复制代码


src/core/network/Downloader.c:

  1. #include "Downloader.h"

  2. int compare(const void* p1,const void* p2){
  3.     return (*(int*) p1)-(*(int*) p2);
  4. }

  5. void mapdownloader(int* sids,int offset,int limit){
  6.     struct stat st={0};
  7.     if(stat("Mapsets",&st)==-1){
  8.         mkdir("Mapsets",0755);
  9.     }
  10.     //To make life easier, sort the items before removing the duplicated.
  11.     int j=1,count;
  12.     qsort(sids,limit-offset,sizeof(int),compare);
  13.     for(int i=1;i<limit-offset;i++){
  14.         if(sids[i]!=sids[i-1]){
  15.             sids[j++]=sids[i];
  16.         }
  17.     }
  18.     count=j;
  19.    
  20.     // TODO: curl_multi
  21.     // It's working well now. If needed, will do multi.
  22.    
  23.     CURL* downloader;
  24.    
  25.     for(int i=0;i<count;i++){
  26.         downloader=curl_easy_init();
  27.         if(downloader){
  28.             struct memory chunk={0};
  29.             char url[60];
  30.             sprintf(url,"https://dl.sayobot.cn/beatmaps/download/full/%d",sids[i]);
  31.             curl_easy_setopt(downloader,CURLOPT_URL,url);
  32.             curl_easy_setopt(downloader,CURLOPT_FOLLOWLOCATION,1);
  33.             curl_easy_setopt(downloader,CURLOPT_WRITEFUNCTION,cb);
  34.             curl_easy_setopt(downloader,CURLOPT_WRITEDATA,(void*) &chunk);
  35.             curl_easy_perform(downloader);
  36.             FILE* fp;
  37.             char fname[20];
  38.             sprintf(fname,"Mapsets/%d.osz",sids[i]);
  39.             fp=fopen(fname,"wb");
  40.             fwrite(chunk.response,sizeof(char),chunk.size,fp);
  41.             fclose(fp);
  42.             free(chunk.response);
  43.             curl_easy_cleanup(downloader);
  44.         }
  45.     }
  46. }
复制代码


因为bplist里面会有重复,所以在下载之前需要对sid进行去重,这里选择先排序后去重,qsort()是C标准库函数。

这里这个程序就非常直白,我想做什么就都写进去了。

这就是目的驱动,我想做什么,都得先算计好,然后去写,这样才能写出一个像样的东西。

后期这个项目应该(?)还会更新,今天就写到这里了。

评分

参与人数 1铁粒 +30 收起 理由
我是redstone + 30 版主推荐

查看全部评分

苦力怕论坛,感谢有您~
回复

使用道具 举报

发表于 2023-9-22 06:28:43 来自手机 | 显示全部楼层 IP:湖南省
认真看完了,但是好像没看懂
2# 2023-9-22 06:28:43 回复 收起回复
苦力怕论坛,感谢有您~
回复 支持

使用道具 举报

发表于 2023-10-27 13:07:25 来自手机 | 显示全部楼层 IP:浙江省
眼熟的代码
3# 2023-10-27 13:07:25 回复 收起回复
苦力怕论坛,感谢有您~
回复 支持

使用道具 举报

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

本版积分规则

本站
关于我们
联系我们
坛史纲要
官方
哔哩哔哩
技术博客
下载
网易版
安卓版
JAVA
反馈
意见建议
教程中心
更多
捐助本站
QQ群
QQ群

QQ群

访问手机版

访问手机版

手机版|小黑屋|系统状态|klpbbs.com

粤公网安备 44200002445329号 | 由 木韩网络 提供云服务 | GMT+8, 2024-4-29 20:51

声明:本站与Mojang以及微软公司没有从属关系

Powered by Discuz! X3.4 粤ICP备2023071842号