PA 3-4 简易文件系统

发布时间:2026/8/3 2:46:45

PA 3-4 简易文件系统
1. 为什么需要文件系统在 PA3-3 中我们使用 ramdisk 存放用户程序但程序一多管理它们的位置就成了问题。更重要的是用户程序自己也需要处理数据比如读取图片、音频它们不可能直接知道数据在 ramdisk 的哪个偏移。因此操作系统必须提供更高层的抽象——文件。文件本质上是字节序列附带一些属性名字、大小、存储位置等。文件系统就是负责管理这些映射并向上提供统一的操作接口。在 Nanos-lite 中我们实现一个极其简化的文件系统sfsSimple File System它有以下限制每个文件大小固定写文件不能超过原有大小文件数量固定不能创建或删除没有目录目录分隔符/只是文件名的一部分这些限制极大降低了实现难度。2. sfs 的存储布局所有文件一个接一个地存放在 ramdisk 中0 --------------------------------------------- | file0 | file1 | ...... | filen | --------------------------------------------- \ / \ / \ / size0 size1 sizen 为了记录每个文件的名字、大小和偏移我们维护一个文件记录表。在代码中它是一个Finfo数组由nanos-lite/src/files.h自动生成通过 Makefile 脚本从navy-apps/fsimg/目录内容生成。例如// nanos-lite/src/files.h (自动生成){/bin/hello,37664,0},{/bin/bird,182976,830504},...3. 文件记录表与文件描述符在fs.c中file_table数组的前几项是特殊文件enum{FD_STDIN,FD_STDOUT,FD_STDERR,FD_EVENTS,FD_DISPINFO,FD_FB};staticFinfo file_table[]__attribute__((used)){[FD_STDIN]{stdin,0,0,0,invalid_read,invalid_write},[FD_STDOUT]{stdout,0,0,0,invalid_read,serial_write},[FD_STDERR]{stderr,0,0,0,invalid_read,serial_write},[FD_EVENTS]{/dev/events,0,0,0,events_read,invalid_write},[FD_DISPINFO]{/proc/dispinfo,0,0,0,dispinfo_read,invalid_write},[FD_FB]{/dev/fb,0,0,0,invalid_read,fb_write},#includefiles.h// 普通文件};Finfo结构体定义了文件的所有信息typedefstruct{char*name;// 文件名size_tsize;// 文件大小size_tdisk_offset;// 在 ramdisk 中的偏移size_topen_offset;// 当前读写偏移量ReadFn read;// 读函数指针NULL 则使用默认 ramdisk 读WriteFn write;// 写函数指针NULL 则使用默认 ramdisk 写}Finfo;disk_offset在初始化时就固定了。open_offset记录文件当前读写位置每次读写后增加。read/write函数指针用于实现虚拟文件系统VFS让不同文件有不同的读写方式普通文件直接用 ramdisk设备文件则调用设备驱动。4. 文件操作实现4.1fs_openintfs_open(constchar*pathname,intflags,intmode){for(inti0;iLENGTH(file_table);i){if(strcmp(file_table[i].name,pathname)0){file_table[i].open_offset0;// 重置偏移returni;// 返回文件描述符即数组下标}}return-1;// 未找到根据需求可触发 panic}因为文件数量固定我们直接遍历文件表匹配名字后返回下标作为文件描述符fd。flags和mode在 sfs 中忽略。4.2fs_readsize_tfs_read(intfd,void*buf,size_tlen){if(fdFD_STDERR)return0;// stdin/stdout/stderr 不支持读// 对于特殊文件有自定义 read 函数直接调用if(file_table[fd].read!NULL)returnfile_table[fd].read(buf,file_table[fd].open_offset,len);// 普通文件从 ramdisk 读取size_treal_lenmin(len,file_table[fd].size-file_table[fd].open_offset);ramdisk_read(buf,file_table[fd].disk_offsetfile_table[fd].open_offset,real_len);file_table[fd].open_offsetreal_len;returnreal_len;}边界检查不能读取超过文件大小的数据。使用open_offset实现顺序读写同时支持lseek调整位置。4.3fs_writesize_tfs_write(intfd,constvoid*buf,size_tlen){if(fdFD_STDIN)return0;// 特殊文件有 write 函数指针if(file_table[fd].write!NULL)returnfile_table[fd].write(buf,file_table[fd].open_offset,len);// 普通文件size_treal_lenmin(len,file_table[fd].size-file_table[fd].open_offset);ramdisk_write(buf,file_table[fd].disk_offsetfile_table[fd].open_offset,real_len);file_table[fd].open_offsetreal_len;returnreal_len;}4.4fs_lseeksize_tfs_lseek(intfd,size_toffset,intwhence){if(fdFD_STDERR)return0;longnew_offfile_table[fd].open_offset;switch(whence){caseSEEK_SET:new_offoffset;break;caseSEEK_CUR:new_offoffset;break;caseSEEK_END:new_offfile_table[fd].sizeoffset;break;}if(new_off0)new_off0;if(new_off(long)file_table[fd].size)new_offfile_table[fd].size;file_table[fd].open_offsetnew_off;returnnew_off;}4.5fs_closeintfs_close(intfd){return0;// sfs 没有打开/关闭状态直接返回成功}5. 让 loader 使用文件原先 loader 直接调用ramdisk_read现在改为使用文件系统接口intfdfs_open(/bin/hello,0,0);fs_read(fd,ehdr,sizeof(ehdr));...更换用户程序时只需修改naive_uload()中传入的文件名即可。6. 一切皆文件与虚拟文件系统VFS6.1 思想Unix 哲学中“一切皆文件”设备、管道、进程信息等都可以视为字节序列通过统一的文件接口open/read/write/close操作。Nanos-lite 将 IOE 设备也抽象成文件串口 →/dev/tty通过stdout/stderr描述符键盘事件 →/dev/events帧缓冲 →/dev/fb屏幕信息 →/proc/dispinfo这样做的好处是用户程序无需知道设备类型一律使用read()/write()访问通过 VFS 分派到具体的驱动函数。6.2 VFS 的实现函数指针Finfo中的read和write函数指针是实现 VFS 的关键。如果指针为NULL则认为是普通文件走 ramdisk 读写否则调用自定义函数。init_fs()中会初始化/dev/fb的大小voidinit_fs(){AM_GPU_CONFIG_T cfgio_read(AM_GPU_CONFIG);file_table[FD_FB].sizecfg.width*cfg.height*4;}7. 设备抽象的具体实现7.1 串口输出stdout和stderr的写函数都指向serial_writesize_tserial_write(constvoid*buf,size_toffset,size_tlen){for(size_ti0;ilen;i)putch(((constchar*)buf)[i]);returnlen;}offset被忽略因为串口是字符设备没有位置概念。7.2 时钟我们提供一个gettimeofday系统调用用于获取当前时间。在 Nanos-lite 内核中size_tsys_gettimeofday(structtimeval*tv,structtimezone*tz){AM_TIMER_UPTIME_T uptimeio_read(AM_TIMER_UPTIME);tv-tv_secuptime.us/1000000;tv-tv_usecuptime.us%1000000;return0;}NDL 库中基于它实现NDL_GetTicks()uint32_tNDL_GetTicks(){structtimevaltv;gettimeofday(tv,NULL);returntv.tv_sec*1000tv.tv_usec/1000;}7.3 键盘事件/dev/events按键事件用文本表示kd 键名\n按下或ku 键名\n松开。在events_read中实现size_tevents_read(void*buf,size_toffset,size_tlen){AM_INPUT_KEYBRD_T keyio_read(AM_INPUT_KEYBRD);if(key.keycodeAM_KEY_NONE)return0;constchar*namekeyname[key.keycode];inttotal_lensnprintf(buf,len,%s %s\n,key.keydown?kd:ku,name);returntotal_len;}该文件不需要lseek字符设备。7.4 屏幕信息/proc/dispinfosize_tdispinfo_read(void*buf,size_toffset,size_tlen){AM_GPU_CONFIG_T cfgio_read(AM_GPU_CONFIG);returnsnprintf(buf,len,WIDTH:%d\nHEIGHT:%d\n,cfg.width,cfg.height);}NDL 中NDL_OpenCanvas通过读取这个文件获取屏幕分辨率例如voidNDL_OpenCanvas(int*w,int*h){intfdopen(/proc/dispinfo,0,0);charbuf[128];read(fd,buf,sizeof(buf));sscanf(buf,[WIDTH] : [%d]\n[HEIGHT] : [%d],screen_w,screen_h);if(*w0*h0){*wscreen_w;*hscreen_h;}}7.5 帧缓冲/dev/fb与像素绘制详解帧缓冲framebuffer本质上是一块连续内存每个像素用 4 字节32 位颜色00RRGGBB表示所有像素按行优先顺序线性排列。屏幕坐标系以左上角为原点(0,0)x 轴向右增大y 轴向下增大屏幕坐标系与字节偏移 (0,0) ────────────────────────────────────► x │ │ (x,y) ●──────────────────────┐ │ │ │ │ │ 矩形区域 │ h │ │ (宽度 w) │ │ │ │ │ └──────────────────────┘ │ (xw-1, yh-1) │ │ 从原点(0,0)开始每行 W 个像素每个像素 4 字节。 │ 像素 (x,y) 的字节偏移为 │ offset (y * W x) * 4 │ ▼ y反过来给定字节偏移offset可反推坐标pixel_index offset / 4; x pixel_index % W; y pixel_index / W;用户程序调用NDL_DrawRect(pixels, x, y, w, h)时该函数逐行将像素写入/dev/fbvoidNDL_DrawRect(uint32_t*pixels,intx,inty,intw,inth){intfdopen(/dev/fb,0,0);for(inti0;ih;i){intoffset4*((yi)*screen_wx);lseek(fd,offset,SEEK_SET);write(fd,(void*)((uint8_t*)pixelsi*w*4),w*4);}close(fd);}内核收到write后VFS 将请求交给fb_write它把字节偏移转换为屏幕坐标并调用 AM 绘图接口size_tfb_write(constvoid*buf,size_toffset,size_tlen){AM_GPU_CONFIG_T cfgio_read(AM_GPU_CONFIG);intx(offset%(cfg.width*4))/4;intyoffset/(cfg.width*4);io_write(AM_GPU_FBDRAW,x,y,(void*)buf,len/4,1,true);return0;}AM 层将像素数据拷贝到硬件的帧缓冲地址void__am_gpu_fbdraw(AM_GPU_FBDRAW_T*ctl){intxctl-x,yctl-y,wctl-w,hctl-h;uint32_t*pixelsctl-pixels;uint32_t*fb(uint32_t*)(uintptr_t)FB_ADDR;uint32_tscreen_winl(VGACTL_ADDR)16;for(inti0;ih;i){uint32_t*dstfb(yi)*screen_wx;uint32_t*srcpixelsi*w;memcpy(dst,src,w*sizeof(uint32_t));}if(ctl-sync)outl(SYNC_ADDR,1);}完整调用链如图所示应用层 (Navy-apps / NDL) ┌─────────────────────────────────────────────────────────────────────────────┐ │ bmp-test 用户程序 │ │ │ │ void *bmp BMP_Load(/share/pictures/projectn.bmp, w, h); │ │ NDL_OpenCanvas(w, h); ← 获取屏幕尺寸 │ │ NDL_DrawRect(bmp, 0, 0, w, h); ← 绘制整张图片 │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ NDL_DrawRect(pixels, x, y, w, h) │ │ │ │ int fd open(/dev/fb, 0, 0); │ │ for (int i 0; i h; i) { │ │ int offset 4 * ((y i) * screen_w x); ← 计算第 i 行字节偏移 │ │ lseek(fd, offset, SEEK_SET); ← 定位到该行起始位置 │ │ write(fd, pixels i*w*4, w*4); ← 写入整行像素数据 │ │ } │ │ close(fd); │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ┌────────────────┼────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ open() │ │ lseek() │ │ write() │ │ 系统调用 │ │ 系统调用 │ │ 系统调用 │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 内核层系统调用分发 (do_syscall) │ │ │ │ case SYS_open: → fs_open(/dev/fb, ...) → 返回 fdFD_FB (即 5) │ │ case SYS_lseek: → fs_lseek(fd, offset, SEEK_SET) → 设置 open_offset │ │ case SYS_write: → fs_write(fd, buf, len) │ │ │ │ │ ▼ │ │ file_table[fd].write ! NULL ? │ │ │ │ │ ▼ │ │ fb_write(buf, open_offset, len) │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ fb_write(buf, offset, len) │ │ │ │ AM_GPU_CONFIG_T cfg io_read(AM_GPU_CONFIG); ← 获取屏幕宽度 │ │ int x (offset % (cfg.width * 4)) / 4; ← 字节偏移 → x 坐标 │ │ int y offset / (cfg.width * 4); ← 字节偏移 → y 坐标 │ │ io_write(AM_GPU_FBDRAW, x, y, buf, len/4, 1, true); │ │ │ │ │ │ │ │ │ │ │ └─ sync true │ │ │ │ └─ w 1 (一次一行) │ │ │ └─ h len/4 (像素数) │ │ └─ 起始坐标 (x, y) │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ AM 层__am_gpu_fbdraw (被编译成 RISC-V 指令) │ │ │ │ uint32_t *fb (uint32_t *)(uintptr_t)FB_ADDR; ← FB_ADDR 0xa1000000 │ │ uint32_t screen_w inl(VGACTL_ADDR) 16; ← VGACTL_ADDR 0xa0000100│ │ │ │ for (int i 0; i h; i) { │ │ uint32_t *dst fb (y i) * screen_w x; ← 计算目标地址 │ │ uint32_t *src pixels i * w; ← 计算源地址 │ │ memcpy(dst, src, w * sizeof(uint32_t)); ← 写入显存通过 MMIO │ │ } │ │ │ │ if (ctl-sync) outl(SYNC_ADDR, 1); ← SYNC_ADDR 0xa0000104通知刷新 │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 硬件层 (NEMU 模拟的 VGA) │ │ │ │ vmem 数组接收像素数据 │ │ vga_update_screen() 检测到 sync 标志 │ │ → SDL_UpdateTexture / SDL_RenderPresent │ │ → 宿主机窗口刷新显示 │ └─────────────────────────────────────────────────────────────────────────────┘8. 系统调用接口在do_syscall()中增加以下分支caseSYS_open:c-GPRxfs_open((constchar*)a[1],a[2],a[3]);break;caseSYS_read:c-GPRxfs_read(a[1],(void*)a[2],a[3]);break;caseSYS_write:c-GPRxfs_write(a[1],(constvoid*)a[2],a[3]);break;caseSYS_lseek:c-GPRxfs_lseek(a[1],a[2],a[3]);break;caseSYS_close:c-GPRxfs_close(a[1]);break;用户层 libos 中的_write、_read等会通过_syscall_调用它们从而让应用程序可以使用标准 C 库的文件操作。9. 测试与运行通过以下命令生成整合了 Navy-apps 全部文件的 ramdisk 镜像makeARCHriscv32-nemu update然后运行文件系统测试makeARCHriscv32-nemu run并在 Nanos-lite 中加载/bin/file-test若一切正确会输出PASS!!!。10. 总结PA3-4 的简易文件系统 sfs 虽然简陋但它体现了文件系统的核心概念文件抽象、描述符、偏移量管理、VFS 与设备文件。通过将串口、键盘、屏幕等设备建模为文件Nanos-lite 实现了一切皆文件的 Unix 哲学为上层应用程序提供了统一简洁的接口。同时VFS 中函数指针的使用展示了用 C 语言实现多态的方法也为后续扩展更复杂的文件系统奠定了基础。

相关新闻

聚宽到QMT或PTrade的故障证据:日志出口要按责任链检查

聚宽到QMT或PTrade的故障证据:日志出口要按责任链检查

2026/8/3 2:36:44

研究信号从聚宽、米筐、优矿或掘金量化流向迅投QMT、恒生PTrade时,一次“没有下单”可能出在六个位置:研究任务没完成、输出文件为空、文件传递失败、账户权限不满足、委托被拒或回报没有写回。牛股王股票更适合普通投资者在股票和ETF回测、盯盘提醒与风…

Milvus

Milvus

2026/8/3 2:36:44

概述 Milvus是一个云原生向量数据库,采用存算分离架构,支持向量相似度与标量过滤的混合查询,能在毫秒级完成十亿级向量检索,并可按需选择强一致或最终一致等多种一致性等级。 从业务角度,milvus的数据模型层级是&…

Unity模型导入变黑?FBX材质与渲染管线兼容性全解析

Unity模型导入变黑?FBX材质与渲染管线兼容性全解析

2026/8/3 2:36:44

1. 项目概述:当模型在Unity中“隐身”时 如果你也曾在Unity中满怀期待地导入一个精心制作的FBX模型,结果却发现它变成了一团漆黑、毫无细节的“煤球”,那么恭喜你,你遇到了3D美术与游戏引擎协作中最经典、也最令人头疼的“模型导入…

ThreadLocal 不是银弹:一次内存泄漏把老年代打满的复盘

ThreadLocal 不是银弹:一次内存泄漏把老年代打满的复盘

2026/8/3 6:16:54

引子:为什么人人都用 ThreadLocalWeb 开发里 ThreadLocal 几乎无处不在:Spring 的 RequestContextHolder、MyBatis 的 PageHelper、日志链路追踪的 traceId,底层都靠它把"只属于当前请求"的数据绑在线程上,免去一层层传…

学生党如何低成本选择AI工具:去AIGC与率零对比

学生党如何低成本选择AI工具:去AIGC与率零对比

2026/8/3 6:16:54

1. 项目概述:学生党如何低成本选择AI工具去年我在准备毕业论文时,曾经连续两周每天花5小时对比各种AI工具的价格和性能。作为每月生活费只有1500元的穷学生,我深刻理解在预算有限的情况下选择AI工具的纠结。现在市面上主流的"去AIGC&quo…

Klick‘r深度解析:Android图像识别自动点击工具架构剖析

Klick‘r深度解析:Android图像识别自动点击工具架构剖析

2026/8/3 6:16:54

Klickr深度解析:Android图像识别自动点击工具架构剖析 【免费下载链接】Smart-AutoClicker An open-source auto clicker on images for Android 项目地址: https://gitcode.com/gh_mirrors/smar/Smart-AutoClicker Klickr是一款基于图像识别技术的Android自…

Java SPI 被 Spring 惯坏后:ServiceLoader 源码里 3 个把人坑哭的实例化细节

Java SPI 被 Spring 惯坏后:ServiceLoader 源码里 3 个把人坑哭的实例化细节

2026/8/3 6:16:54

引子:为什么一堆框架都爱用 SPIJava 标准库里有个不起眼的接口加载机制:Service Provider Interface。你在 classpath 下放一个 META-INF/services/全限定接口名 的文件,里面写几个实现类的全限定名,运行时调用 ServiceLoader.loa…

Unity万向锁问题解析与四元数解决方案实战

Unity万向锁问题解析与四元数解决方案实战

2026/8/3 6:16:54

1. 项目概述:一个被忽视的“旋转陷阱”如果你在Unity里做过稍微复杂一点的3D角色动画,尤其是涉及到多个轴向(比如脖子、肩膀、脊柱)的旋转控制时,很可能遇到过一种诡异的现象:明明只想让模型绕一个轴旋转&a…

SpringBoot+Vue选课系统架构设计与性能优化

SpringBoot+Vue选课系统架构设计与性能优化

2026/8/3 6:06:53

1. 项目概述:前后端分离选课系统的技术架构这个学生选课系统采用了典型的前后端分离架构,后端使用SpringBoot框架提供RESTful API接口,前端通过Vue.js构建用户界面,数据持久层采用MyBatis操作MySQL数据库。这种架构模式在当前企业…

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案

2026/8/3 4:49:52

ncmdumpGUI:一键解锁网易云音乐ncm文件的终极解决方案 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 你是否曾经从网易云音乐下载了心爱的歌曲&am…

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比

2026/8/2 0:04:43

分布式配置中心选型实战:Nacos与Consul在创业场景下的对比工程导读:本文深入讨论 分布式配置中心选型实战:Nacos与Consul在创业场景下的对比 在生产工程实践中的核心落地方案。基于 分布式架构与微服务设计 视角,剖析实际痛点、架…

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案

2026/8/2 0:04:43

MoneyPrinterPlus实战指南:AI视频批量生成与自动化发布完整解决方案 【免费下载链接】MoneyPrinterPlus AI一键批量生成各类短视频,自动批量混剪短视频,自动把视频发布到抖音,快手,小红书,视频号上,赚钱从来没有这么容易过! 支持本地语音模型chatTTS,fasterwhisper,…

从提示词小白到AI内容架构师(20年技术老兵的6阶能力跃迁图谱,仅剩最后87个免费解读名额)

从提示词小白到AI内容架构师(20年技术老兵的6阶能力跃迁图谱,仅剩最后87个免费解读名额)

2026/8/3 0:06:20

更多请点击: https://codechina.net 第一章:AI写作能力跃迁的认知革命 过去五年,AI写作已从“模板填充”迈入“语义共建”阶段——模型不再仅复述训练数据中的句式,而是基于跨文档推理、意图锚定与风格自适应,动态构建…

AU-48八米拾音的信噪比衰减与降噪门限耦合分析

AU-48八米拾音的信噪比衰减与降噪门限耦合分析

2026/8/3 0:06:20

一、"拾音 8 米"这个指标该怎么读AU-48 的规格里,麦克风拾取范围写的是 10cm-800cm,配合 T1/T2 参数切换可选四档:中距离 0.5-2m、近距离 0.1-0.2m、远距离 0.5-5m、超远距离 0.5-8m。"能拾音 8 米"这句话本身没错&#…

LangChain 从 Demo 到团队落地,真正卡壳的是哪一步?

LangChain 从 Demo 到团队落地,真正卡壳的是哪一步?

2026/8/3 0:06:20

聊《LangChain并不难,难的是知道什么时候不该用》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。 摘要 摘要:很多人学 LangChain 都是从调个 API 开始,跑通一个 Demo 觉得挺简单…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/2 17:06:42

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/2 5:08:03

2026年真正好用的AI论文工具,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/3 2:41:27

告别游戏崩溃:XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…