目录
前言
我已经一年多没有更新博客了。 原因是疫情期间发售了《骑马与砍杀2》,然后就去写游戏MOD了。
我用C#写了游戏MOD大约7个月,每天晚上熬夜。 这期间,我介绍了这个游戏MOD,学习了PR,然后成为了B站的UP主。
后来我有了一些其他想法和公司业务调整,就懒得写博客了。 不知不觉,一年多过去了。
还是有收获的:
好吧,让我们言归正传。
现在Mod更新基本上已经停止了,UP主也懒得继续认真做下去了。
这里主要是讲技术,就是一个纯前端的实现,一个写MOD的XML在线编辑器。
它是一个仿风格的编辑器,可以自动学习游戏MOD文件生成的约束规则,帮助我们实现代码提示和代码验证。
更重要的是它可以直接修改你电脑上的文件。
这是最终产品的代码存储库:
以及成品图:
本博客涉及的技术:
让我们从头开始。
对在线 XML 编辑器的需求
在制作《秋千2》的MOD时,需要经常编写XML文件。
因为七巧2的数据配置都是以XML的形式保存的,然后MOD加载后,用MOD的XML来覆盖官方的XML。
通常我们做MOD数据的时候,都是参考官方的XML,自己编写XML文件。
但这会遇到一个问题。 XML没有代码提示和代码验证,很难发现错误的字符。
或者有时游戏会更新,其 XML 规则可能会发生变化。
官方不会发布通知告诉你这些变化,所以如果你仍然使用之前的元素和属性,那就说明你写错了。
写入错误的结果往往是加载MOD时游戏直接崩溃,而且不会给出任何提示。 只能慢慢找bug。
《霹雳游侠2》作为一款大型游戏,每次启动的时间都比较长,因此测试MOD数据是否配置正确的测试过程会很长。
天哪,很多个夜晚,游戏崩溃的那一刻,我就崩溃了。
于是我就想到做一个XML在线编辑器来解决这个问题。
技术预研可视化编程
其实我一开始并没有制作这个XML编辑器的想法,因为这个东西一看就很难用。 相反,我想通过可视化编程和拖动元素和属性来实现它。
你别告诉我,我其实做了一个初步的计划,但是最终配置了一个很大的XML东西,拖了无数次,我的心态逐渐爆炸,所以我放弃了这个计划。
插入
我想看看有没有可以提供代码提示的插件。 有一个使用XSD进行代码验证,好像是IBM提供的。
但可惜的是已经废弃了,不能再使用了,所以这个计划也就废弃了。
在线编辑器
后来之所以用在线编辑器来做这个,是因为抖音()想做一个在线编辑Java项目环境xml配置文件的东西。
然后我尝试在这里制作一个并了解它。
您可以自行配置标签支持XML代码提示,但不支持XML代码验证,因此需要您自己进行XML代码验证。
并且因为我们通常使用xsd来验证xml,所以我们还需要将xsd转换为标签配置。
不管是百度还是百度都没有相应的解决方案,所以只能自己写代码来实现。
在这个过程中,我对xsd有了更深入的了解,并最终完成了项目。
因为这是之前公司的代码,这里就不公布了。
总之,在过程中了解到这样的事情后,我就产生了使用在线编辑器进行MOD的想法。
初始形式:简单的在线XML编辑器
好了,废话不多说。 拿起键盘简直就是无脑的事情。
最初的形式,左侧没有文件树,只有一个简单的编辑器和一个规则学习弹出框。
只涉及三种技术:
用作编辑器
这块主要用的是react-的封装版本,反正看文档自己用demo配置一下就可以了。
唯一的困难是,网上有很多配置介绍,很多都是复制转载的,但还是错的,简直令人发指。
总之,想玩的话最好看一下官方文档()以及文档中的demo,然后自己研究一下。 如果照搬别人的配置,水深了,你抓不住。
这里贴出一段我封装的编辑器组件的配置代码。 无论如何,绝对可以用。 大多数编辑器的功能都还可以,但只适合编辑XML。
里面的注释相当详细,包括常用的代码折叠和代码格式化。 我懒得一一解释。 您可以参考官方网站亲自查看。
我不会发布一些参考代码。 如果您有兴趣,可以查看上面提到的代码库。
import { useEffect } from 'react' import { Controlled as ControlledCodeMirror } from 'react-codemirror2' import CodeMirror from 'codemirror' import 'codemirror/lib/codemirror.css' import 'codemirror/theme/ayu-dark.css' import 'codemirror/mode/xml/xml.js' // 光标行代码高亮 import 'codemirror/addon/selection/active-line' // 折叠代码 import 'codemirror/addon/fold/foldgutter.css' import 'codemirror/addon/fold/foldcode.js' import 'codemirror/addon/fold/xml-fold.js' import 'codemirror/addon/fold/foldgutter.js' import 'codemirror/addon/fold/comment-fold.js' // 代码提示补全和 import 'codemirror/addon/hint/xml-hint.js' import 'codemirror/addon/hint/show-hint.css' import './hint.css' import 'codemirror/addon/hint/show-hint.js' // 代码校验 import 'codemirror/addon/lint/lint' import 'codemirror/addon/lint/lint.css' import CodeMirrorRegisterXmlLint from './xml-lint' // 输入> 时自动键入结束标签 import 'codemirror/addon/edit/closetag.js' // 注释 import 'codemirror/addon/comment/comment.js' // 用于调整codeMirror的主题样式 import style from './index.less' // 注册Xml代码校验 CodeMirrorRegisterXmlLint(CodeMirror) // 格式化相关 CodeMirror.extendMode("xml", { commentStart: "", newlineAfterToken: function (type, content, textAfter, state) { return (type === "tag" && />$/.test(content) && state.context) || /^ { // tags 每次变动时,都会重新改变校验规则 CodeMirrorRegisterXmlLint(CodeMirror, tags, one rrors) }, [onErrors, tags]) // 开始标签 function completeAfter(cm, pred) { if (!pred || pred()) setTimeout(function () { if (!cm.state.completionActive) cm.showHint({ completeSingle: false }); }, 100); return CodeMirror.Pass; } // 结束标签 function completeIfAfterLt(cm) { return completeAfter(cm, function () { var cur = cm.getCursor(); return cm.getRange(CodeMirror.Pos(cur.line, cur.ch - 1), cur) === "<"; }); } // 属性和属性值 function completeIfInTag(cm) { return completeAfter(cm, function () { var tok = cm.getTokenAt(cm.getCursor()); if (tok.type === "string" && (!/['"]/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length === 1)) return false; var inner = CodeMirror.innerMode(cm.getMode(), tok.state).state; return inner.tagName; }); } return () } export default XmlEditor时自动键入结束元素 toggleComment: true, // 开启注释 // 折叠代码 begin lineWrapping: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], // 折叠代码 end extraKeys: { // 代码提示 "'<'": completeAfter, "'/'": completeIfAfterLt, "' '": completeIfInTag, "'='": completeIfInTag, // 注释功能 "Ctrl-/": (cm) => { cm.toggleComment() }, // 保存功能 "Ctrl-S": (cm) => { onSave() }, // 格式化 "Shift-Alt-F": (cm) => { const totalLines = cm.lineCount(); cm.autoFormatRange({ line: 0, ch: 0 }, { line: totalLines }) }, // Tab自动转换为空格 "Tab": (cm) => { if (cm.somethingSelected()) {// 选中后整体缩进的情况 cm.indentSelection('add') } else { cm.replaceSelection(Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input") } } }, // 代码提示 hintOptions: { schemaInfo: tags, matchInMiddle: true }, lint: true }} editorDidMount={onGetEditor} onBeforeChange={onChange} />
学习XML并提取标签规则
当我们使用简单的编辑器并想要执行XML代码提示时,我们需要使用标签。
显然,不同的游戏有不同的XML规则,包括游戏更新后会改变的XML规则。
所以我们必须保证有一个机制来不断的学习这些XML规则,所以这里我做了一个学习XML文件规则的弹窗来做到这一点。
点击编辑器左上角的约束规则——>添加新的约束规则
会弹出一个这样的弹窗:
通过读取指定文件夹中的XML文件,然后使用顺序解析这些xml文件的文本来生成文档对象。
然后对这些文档对象进行分析,得到最终的标签规则。
这一步只需要对xml有一定的了解即可。 这实际上是非常基本的,所以我不会深入讨论。
简而言之,现在我们已经完成了它的初步形式。 每次使用时,您都需要将您编辑的XML文件的内容复制到此在线编辑器中。 编辑完成后,将完成的文本复制到原始XML文件中,保存并覆盖。
进化形式:加载树形文件结构和完整文件验证功能的在线XML编辑器
上面的编辑器实际上使用场景非常狭窄,只能在编写新的XML时使用。
一个MOD往往包含几十个、几百个、甚至上千个文件,不可能将它们一一粘贴到编辑器中进行验证。
所以我们需要在这个编辑器中加载MOD的所有XML文件并进行代码验证。
只涉及两种技术:
左边的文件树
左边的文件树是使用Ant的Tree组件完成的。 这里我就不赘述配置了。
单击按钮打开文件夹时
也用于读取MOD文件夹中的文件。
但得到的是一个文件数组。 如果我们想要生成左边的树结构,我们需要手动解析每个XML文件的路径,并生成相应的树结构。
完整的文件验证功能
打开文件夹的那一刻,我们需要对所有 XML 文件执行代码验证。 如果验证不正确,我们需要将相关文件及其父级和祖先的一系列文件夹复制到左侧文件夹中。 标记为红色。
这个功能表面上很简单,但实际上它有很多陷阱,因为验证的计算量其实不小,尤其是当你的MOD中有成百上千个文件时,很容易导致你的被屏蔽的js和被屏蔽的页面。 没有反应。
这里我使用Web新开一个线程来处理验证过程,验证完成后将结果返回给我。
在这个过程中,我也了解了更多关于Web的使用。
一直以为是一种新的(某个js文件)的玩法,但是结合react的模块化开发使用似乎很难。
但其实现在在配置上,使用Web是非常方便的。
首先,我们的代码可以写成如下:
import { lintFileTree } from '@/utils/files' onmessage = ({ data }) => { lintFileTree(data.fileTree, data.currentTags).then(content => { postMessage(content) }) }
那么我们在使用的时候可以这样操作
import { useWebWorkerFromWorker } from 'react-webworker-hook' import lintFileTreeWorker from '@/utils/webWorker/lintFileTree.webworker' const worker4LintFileTree = new lintFileTreeWorker() const [lintedFileTree, startLintFileTree] = useWebWorkerFromWorker(worker4LintFileTree)
然后你使用对此的依赖,并在它发生变化时执行某些操作,因此编写就像使用它一样简单。
非递归树遍历
可以看到我们上面用到的很多东西都和树有关,比如遍历文件树来验证代码。
或者也许我们切换某个约束规则后,需要遍历整个文件树来重新验证。
在遍历的过程中,我习惯了递归遍历整棵树。 这样做的缺点是递归时无法释放内存,所以后来我改变了算法,使用非递归的方法来遍历整棵树。
保存文件内容
因为我们的MOD文件包含的内容很多而且很大,所以内存占用可能会很大,不可能将这些文件的内容保留在内存中。
所以我读到的文件内容都会一一放进去,只显示当前编辑的文件内容。
只有在需要时,例如完整文件验证或文件切换,才会再次检索文件内容。
终极进化形态:突破浏览器沙箱限制,实现电脑本地文件的增删改查
通过前面的操作,我们终于完成了一个基本可用的在线XML编辑器。
然而它有一个致命的缺陷,那就是受到浏览器沙箱环境的限制。 我们修改文件后,并不能直接保存到电脑中。 我们必须手动将修改后的代码一一复制到对应的文件中。
这个操作繁琐复杂,所以我们编辑器的功能可能只能用来辅助代码编写和批量验证。
本以为只能做到这个程度了,后来无意中看了知乎上的一个帖子,发现+版本多了一个功能性API:。
另外,除非是本地环境,否则该API只能在https环境下调用。 也就是说,如果你是http网站,即使你使用+或者+,也是无法调用的。
这个API可以让我们直接操作本地计算机上的文件,而不是仅仅读取它们或者只在浏览器沙箱中操作它们。
通过这个我们不仅可以读取和修改文件夹中的文件,还可以添加和删除文件。
于是我用这个API完全替代了我之前使用的所有点,通过右键单击文件树实现了文件夹和文件的添加和删除。 (这里不支持重命名文件,但实际上我们可以通过删除然后添加新的方式来模拟重命名,但我懒得这样做)
同时,按下保存按钮或按保存快捷键Ctrl+S后,可以直接保存文件。
这是使用打开文件夹的组件代码:
import React from 'react'
// 自定义的打开文件夹组件
const FileInput = (props) => {
const { children, onChange } = props
const handleClick = async () => {
const dirHandle = await window.showDirectoryPicker()
dirHandle.requestPermission({ mode : "readwrite" })
onChange(dirHandle)
}
return
{children}
}
export default FileInput
只要点击该组件包裹的元素(比如按钮),就会立即调用它来请求打开文件夹。
打开文件夹后,通过获取的文件夹请求文件夹写权限,然后将文件夹传输到外部,获取文件树结构。
这里的操作是有缺陷的,因为当请求打开文件夹时,浏览器会弹出一个框,询问用户是否有读取该文件夹的权限。
打开后会第二次弹出该框以获得写权限,也就是说打开该文件夹时会弹出两次该框。
但我通过这个方法只能一次性请求所有权限,不然等到我要保存再请求权限就不好了。
然而,缺陷并没有被掩盖。 通过这个API,不仅实现了文件的增删改查,而且杜绝了文件的使用。
因为我们可以随时通过文件获取对应的文件内容,所以不需要将文件内容保存到.