图解白菜用到的循环队列

实现了用循环队列存QQ消息后,就七八个月没有管(能用的代码才是好代码),以至于写上简历被问到之后支支吾吾说不出……

这个队列比较特殊,只有入队和遍历,没有写出队(业务用不到)。
代码如下(CqMsg为反序列化出来的实体类,记载着QQ号、消息体、发送时间等信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class MsgQueue {

private int start = 0;
private int end = 0;
private int len = 0;
private int N=100;

private CqMsg[] msgs = new CqMsg[N];
public MsgQueue(){}
public MsgQueue(int N) {
this.N=N;
msgs = new CqMsg[N];
}
public void addMsg(CqMsg msg) {
len++;
if (len >= N) {
len = N;
start++;
}
if (end == N) {
end = 0;
}
if (start == N) {
start = 0;
}
msgs[end] = msg;
this.msg = msg;
end++;
}


public ArrayList<CqMsg> getMsgsByQQ(Long QQ) {
ArrayList<CqMsg> result = new ArrayList<>();
if (start < end) {
for (int i = 0; i < end; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
} else {
for (int i = end; i < msgs.length; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
for (int i = 0; i < start - 1; i++) {
if (QQ.equals(msgs[i].getQQ())) {
result.add(msgs[i]);
}
}
}
return result;

}
}

一开始我创建了一个数组(容量由构造器指定),同时定义两个整数作为坐标变量,再加一个表示队列当前长度的变量,大概是这样的:

1
length = 0
null null null null null null
Start
End

之后每当收到新消息,都将消息插入到end坐标上,之后将end++,同时length++,直到数组装满。

在数组装满后,队列是这样的:

1
length = 5
消息1 消息2 消息3 消息4 消息5 消息6
Start
End

当有下一条消息进入队列时,尝试将队列长度增加:

1
length++

随即满足下方if语句块的判定,将length重置为数组长度,并且将起始点右移:

1
2
3
4
if (len >= N) {
len = N;
start++;
}
1
length = 5

进入下一个if块:

1
2
3
if (end == N) {
end = 0;
}

最后把结果插入到end上,之后end++:

消息7 消息2 消息3 消息4 消息5 消息6
Start
End

不断的插入消息后,Start也会达到数组最右端,此时的队列如图:

消息7 消息8 消息9 消息10 消息11 消息6
Start
End

此时插入消息的逻辑如下:

首先依旧将length重置为当前数组长度;

但是不会进第二个if块,直接进第三个:

1
2
3
if (start == N) {
start = 0;
}

这之后执行end++,会回到数组第一次装满时候队列的样子:

消息7 消息8 消息9 消息10 消息11 消息12
Start
End

每次遍历时,只需要从end→length,再从0→start就行,是一次O(n)的操作。

而插入时,则只需要O(1)(判断几个数字的大小、给数组某个位置赋值)。

如改用纯数组实现,在数组满后,插入消息需要O(n)的时间。

若改用链表,则可以使用迭代器达到与循环队列相同的复杂度:

有新消息时,丢弃头部消息,在尾部追加消息,也是O(1)的操作。

遍历时, 并不使用传统的for+get方法(每次获取链表的某个位置的值代价都是O(n)),而是使用迭代器,可以达到O(n)的时间复杂度。

白菜日记6

快半年了,白菜的功能也发生了很多变化。
加入了对接osu search接口,可以提供搜索词来找到自己在某个图的成绩。
加入了其他三个模式的支持(懒得从其他语言移植其他模式的PP计算)。
另外还有cost和bounspp的计算……
记不太清这五个月做了什么了,现在有了AOP的异常通知、参数拦截,redis缓存……
现在说起来好像挺简单,不过实现的时候还是挺开心的。

比赛分析也改成了用命令增删玩家、谱面……

白菜日记5

之前的名字好像太长了(

自动清理文件我直接写死了Linux的路径,反正也不会在开发环境下用到这个功能(
今天折腾了个验证码,初步看了一下Interceptor和Filter的区别,前者是基于AOP来拦截每个Controller请求,本质上是个Aspect,和我用来全局捕捉异常的东西是一样的。。
后者则是J2EE里Servlet的功能,在Spring的DispatcherServlet之前工作,在Controller工作之前就拦截了请求……
虽然其实功能差不多,传入的都是HttpServletRequest/Response,不过我的代码要用到redis,而redis的工具类是托管在Spring里的,所以我还是选了Interceptor。

这个比赛分析功能还是挺有用的……后期考虑改成命令(

晚点改一下分数抓取的目标和继续做登录吧(

白菜的开发日记4

为了节省费用,我尝试将白菜移植到一台必须有Linux环境的机子上运行(为了和节点共存)。
一开始打算使用WSL,无奈这东西在桌面Win10已经普及了,但是Server系统下只有一个Insider Preview。

Preview到什么程度呢,那个系统没有Explorer,甚至没办法移植(comctl32.dll还是哪个系统文件是定制的,没法覆盖也没法兼容,毕竟VPS可没有PE和安全模式)。

在一番挣扎之后我们彻底放弃了它,改为Ubuntu,于是我的灾难来了……

在此之前我也曾经在CentOS等系统下配过Java+MySQL的环境,但是酷Q是用易语言写的,于是就涉及到一个Docker的使用问题。

在坑了一天之后(其实是我耐不下心去啃文档,只能自己瞎几把试),大概了解了这点:
docker pull是下载一个镜像,类似于创建一个类
docker run是创造一个容器,类似于实例化对象
而docker start和stop是操作这个容器,docker rm是删除容器(对象),docker rmi是删除镜像(类)。

大概遇到的坑有:
在docker内运行的酷Q,虽然能提供5700的HTTP Server,但是同一台机子上运行的Tomcat开放的8080端口它却无法作为客户端使用,
指定localhost:8080和127.0.0.1:8080都无效,必须把酷Q的消息上报地址改为服务器的公网IP(我实际上是改为了域名)

我的代码里需要处理ppy给的类似于 1970-01-01 00:00:00的日期格式,
在Win下面,Gson默认转换没有问题,但是Ubuntu下就不行(似乎和时区无关,我本地的WSL也是这样),必须硬编码一个日期格式才能起作用。

这次重构,把CQService里具体的处理拆出来一个CmdUtil,实体类、util全部分包处理,
为了避免Win/Ubuntu的路径不一致将所有素材以二进制形式存到了数据库……四个语音base64编码之后直接硬编码到程序里(

同时还发现,我之前给ImgUtil加了一个static的Map,然后写了个静态方法去用NIO爬目录,把图片塞到那个Image里,最后在构造器里调用这个方法……
但是我ImgUtil是用prototype模式注入,每次有HTTP连接都会实例化一个新的ImgUtil,重新爬一次,压根没起到缓存的作用_(:з」∠)_

还把oppai换成了Java实现,那作者自称不喜欢Java,也别问他要更多Java程序或者支持,虽然oppai的功能全部搬了过去。
用了一下,我发现他用了一大堆静态内部类,而且要使用它 必须把它和调用它的类放在同一个包里(有个关键东西的构造函数是包访问权限的)。
本来想自己改造,但是想到万一PP算法一改,作者一更新那我不就歇逼了?于是老老实实按他规定的方法来_(:з」∠)_

一开始没掌握正确的使用方法,于是昨晚上漏掉了HR和DT(我把MOD应用之前的star穿进去了,柑橘妖怪作者不提供详细的example)。

再一次体会到了“你以为你写的代码是这样跑的,其实根本不是”的心情。

接下来就是写网站部分,然后还有一个准备咕很久的东西……还有白菜的邀请入群机制也要改改,自动清理文件的机制也要改,不过这两天我有点心力憔悴,先咕着吧……

白菜的开发日记3

许久没更博客了,写点白菜最近的进度吧。

整个Web版重构完成,我还在路由器上搭了一个mysql server,然后用mysqldump每天凌晨备份白菜的数据库。

顺手加上了凌晨把获取失败的ID以邮件形式发到自己邮箱,还加了清空当日生成的临时文件。

弱智啪啪啪提了一个req,!sleep 按小时为单位禁言自己,没想到做出来真的一堆弱智用……

日记2里提到的绘图类,被我从静态代码块改成了静态方法,然后在!sudo bg命令时调用它(否则会导致修改用户bg不能马上生效)。

顺手做了一个!fp功能,不过mp4和5没什么人用……

我还研究了一下HTTP Client,做了模拟登陆osu并且下图,还写了解析.osu的正则表达式,来从官网获取BG。

甚至还处理了0,0,”sb\bg.png”这种情况……

在这里吹自己一波,我居然想到了把官网下图时的InputStream包装为ZIPInputStream,直接在内存中解压osz……

写完才发现“卧槽我居然真的把这个想法实现了”。。可以说白菜现在的粗壮性又上了一个台阶(

白菜的开发日记2

白菜之前的代码基本已经稳定运行了,于是我准备把它改成一个Web项目,采用HTTP API来收发消息,同时将来可能扩展出网页什么的……

在重构的时候遇到一个问题:之前我在绘图类,用nio扫出结算界面需要的所有图片,然后用static代码块包裹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static {
final Path resultPath = Paths.get(rb.getString("path") + "\\data\\image\\resource\\result");
//使用NIO扫描文件夹
final List<File> resultFiles = new ArrayList<>();
Images = new ArrayList<>();
Nums = new ArrayList<>();
Mods = new ArrayList<>();
SimpleFileVisitor<Path> resultFinder = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
resultFiles.add(file.toFile());
return super.visitFile(file, attrs);
}
};

try {
//将所有文件分为三个List
java.nio.file.Files.walkFileTree(resultPath, resultFinder);
for (int i = 0; i < 23; i++) {
Images.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 23; i < 37; i++) {
Nums.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 37; i < 48; i++) {
Mods.add(ImageIO.read(resultFiles.get(i)));
}
zPP = ImageIO.read(resultFiles.get(48));
zPPTrick = ImageIO.read(resultFiles.get(49));
} catch (IOException e) {
logger.error("读取result相关资源失败");
logger.error(e.getMessage());
}

这样这些对象只会在这个类第一次被加载的时候生成,避免了每次绘图重复扫描文件夹,降低效率的问题。

然后后面画图的时候,我只需要调用List中的BufferedImage对象

1
2
3
//右下角两个FPS
g2.drawImage(Images.get(1), 1300, 699, null);
g2.drawImage(Images.get(2), 1300, 723, null);

但是如果我要新加功能就会显得不便:我只能添加z开头的文件名,否则我得把所有的Images.get()后面的数字调一下。。

看了一眼它们的文件名,我觉得ppy应该是在osu启动的时候把皮肤全部加载到内存,然后根据文件名绘制……

看了一下nio,好像能生成文件名,那我把List换成Map好了……

白菜的开发日记1

emm。从16号早上出门前Initial Commit,到23号中午12点白菜正式上线,这一周的时间过的真是快啊……

https://github.com/Arsenolite/osubot/commits/master

43次commit之后,终于有时间坐下来写点开发日记了。严格的说这是第一个我写出来能用的程序啊(x

先介绍一下她吧,她是一个面向osu群的QQ机器人,具体用法可以参见github的readme(普通用户就两条指令你还写个readme真是凑不要脸x)

相对别的机器人的亮点就是她返回的是图片,样式主要由啊哇设计,我们从旁修改,当然最后是我实现的。(记得18号那天我画一行丢一个图在群里x

作为Java程序员,老本行就是折腾数据库,当然白菜也有这方面的功能。每天凌晨4点将所有登记了的玩家的数据爬取,然后存入,提供一个对比功能。

18号晚上写完用户名片绘制,19号上午写完凌晨的定时任务,20号做了一些sudo命令。

22号凌晨1点终于搞定BP绘制,然后白天研究maven打包,开通vps,关闭mysql占用空闲内存缓存等等。

22号晚上怕他突然提出来一个大BUG,之前数据库中我存入的是用户名,但是这样会导致无法识别改名玩家的问题。

于是晚上赶工重构代码+改表结构,由于发现操作系统的时区就是北京时间,而BP返回时间是根据API key对应玩家的国籍来的(也是北京时间),砍掉了所有的时区转换。

在vps上运行的时候查API失败率非常低,于是将api工具类中加上重试机制,砍掉了其他地方代码对网络错误的try-catch。

然后就是些小bug(之前改的时候没改彻底导致的各种问题),昨天中午正式上线,下午晚上研究了jsoup爬取网页指定元素,又把爬score rank改成了二分法。

顺手写了个自动欢迎新人,由于啊哇还没想好scorerank的呈现方式,把绘制scorerank的代码注释掉发布到了服务器上。

……挺后悔没有边开发边写博文的,现在有的遇到的bug都已经忘记了……

总之这个项目增强了我对SQL、前端(jsoup提供的是js的getElementById方法,和css的div.Class风格的选择器)的熟练度,顺便还达成了第一次使用WinServer系统的成就,勉强算是linux+WinServer都能干的运维?

复习了以前练手搞的多线程机制,IO流,各种包装类,日期处理,字符串处理之类的基础问题,初次尝试手写二分法(特别新鲜),可以说它弥补了我Java基础代码写得少的缺陷……

毕竟之前在单位搞的SSM框架更多是做填空题,把XML配好,crud和参数验证写好,甚至那个小项目都用不着组装复杂pojo……到处都是提供好的最佳实践(

……虽然这个项目用的也都是给好的类和方法,充其量起到了熟悉JDK本身的作用,毕竟不是和c一样要手写String的查找替换啥的(x

emm,白菜大概是不会弃坑的,以后尽量写出详细的遇到/解决bug的过程吧233