ready for v5

This commit is contained in:
UnitedAirforce
2026-01-11 18:38:14 +08:00
parent e3d7acfbf8
commit 481c9ceab7
11 changed files with 964 additions and 14 deletions
+397 -14
View File
@@ -1,18 +1,84 @@
# OverRide-Server
This is the server & package repo of a OverRapid mod that I made a while back. Open to all since the dev abandoned the game and is no longer adding new content.
这是Project OverRide的服务器和包体仓库。Project OverRide是一个基于OverRapid的模组,由于原开发者长期未更新内容,现公开。
<details>
<summary>English</summary>
<br>
## Why OverRide
This mod brings comprehensive feature updates, content, and bug fixes to the base game.
This non-exaustive list describes some of those.
- Added 14 removed songs (Lag train, rainshower, happy?, cancer, chaotic, after white, overprocess, unfactual factory, euphoric planet, dawn, sudden noise, night ocean fantasy, Metatron rebirth, hymn for U).
- Support for all 32 Arcane charts.
- Added 3 removed BGAs (xmas17, cosmic, decryptor).
- Added 2 removed Companions (doge, xmas17).
- Added 1 removed Gear (xmas17).
- Improved localization for all languages, and the game is localized to Simplified Chinese. (by improve, I mean that most image-based texts are changed to code based for better resolution and easier update. Korean and Japanese is mostly done by Google Translate, which is not ideal)
- Ability to connect to server directly (without proxy software)
- iOS, Android, and PC support.
- "Invisible Note" and speed up/down (0.8x - 1.2x) playstyle
- Added 10 achievements from season challenge 1.
- Added removed BGE for Outer Space.
- Officially supports project OverWrite (Missed charts for old songs) and save optimization.
- Added Hadron Kollider 4kpro (converted from 6k, very hard), and BGE support for all difficulties for H1V3.
- Revamped song unlock mechanism. Packs can be unlocked via RP and more songs are added to the level-up mechanism.
- Optimized and refactored game logic, removed redundant code, and optimized client and server assets.
- Added ability to restore save file via UID input at name prompt. Removed social OAUTH function.
- Supports download all charts and offsets via freeplay console.
- All functionality except leaderboard, download, ratings ranking, and PVP works offline. (must be connected at least once)
- Remade login client user flow. User need to create username first before downloading files.
- Ability to disable account creation, and ban user from server.
- Removed source code compilation and song encryption.
- Fixed multiple issues with TKR skin and allow detailed opacity customization through settings - BGA Opacity, and expanded the applicable song for Lanota BGE.
- TKR theme has been improved - switch over and check it out!
- Added Render The Over Stage theme (old application theme) that supports all modern features.
- Built in full fanmade chart support. Import on server, play on client solo or multiplayer immediately.
- General bug fixes.
## The Server
Download the code and asset, unzip the asset in the server root directory.
## The Asset
There are two folders of assets that you must download before the server is functional.
There are three folders of assets that you must set up before the server is functional.
They are all placed under the server root directory.
`OverRapid`: Contains music, chart, BGA, and config files. They are stable, and incremental.
`OverRide`: Skeleton folder for fanmade content. Typically, you don't need to mess with it.
`Resource`: Contains basic assets, such as images, movies, and transition audios. This is volatile, and the content inside can shift around as version progress.
Below are the Google Drive and Baidu Drive link to a folder, containing two zip files.
@@ -27,12 +93,6 @@ V4 Asset: [Google Drive](https://drive.google.com/drive/folders/1jR37N2wi6M1l0Eq
After this, follow the Manual.
## The Client
Simply download the client, install/sideload, and run. Follow the UI to enter the `http(s)://HOST:PORT/`, and click continue.
Note. The package released carries the same security level as the official packages, from the official stores. Don't snoop around, but certainly have fun!
## Manual
<details>
@@ -41,13 +101,13 @@ Note. The package released carries the same security level as the official packa
### Preface
Please join QQ group (511974777) to report issues. It is not encouraged to use the private server to play the official content. Help will not be provided for this.
Please join QQ group (511974777) or use the issues section to report server and client issues.
### Server Setup and Connection
Download the server and respective platform's installation package from the group files.
Download the server and respective platform's installation package from the group files/github release.
Unzip the server to your PC or MAC (referred to as the machine) and install the application package to your mobile device (referred to as the device). Linux users should help themselves lol
Unzip the server to your PC or MAC (referred to as the machine) and install the application package to your mobile device (referred to as the device). ~~Linux users should help themselves lol~~
Android package has been renamed and icon replaced to allow better distinction with the official client. You need jailbroken devices and tools such as trollstore to install on iOS.
@@ -60,7 +120,7 @@ python3 get-pip.py
Note that MAC uses ```python3```. Code examples in this document will use the default of Windows, which is ```python```. After the installation, install dependencies using ```pip install requirements.txt```
After it is installed, make sure your machine and device are under the same subnetwork, say, wifi. Open ```cmd``` on PC and type ```ipconfig```. Open ```terminal``` on MAC and type ```ifconfig```.
After it is installed, make sure your machine and device are under the same subnetwork, such as wifi. Open ``cmd`` on PC and type ``ipconfig``. Open ``terminal`` on MAC and type ``ifconfig``.
Use the output to find the machine's subnet IPV4. Open ```config.py``` inside the server folder and change the IP address to the one you just got. Change the port as you please.
@@ -82,6 +142,10 @@ Go to the settings page on your old device. Click the "Account" text under the "
You can also find the ```UID``` by opening ```player.db``` using ```DB Browser```.
#### Adding currency
Currently, the game grants unlimited currency. In case that was changed in the future, you can issue a redemption code in the database with the following `item`: `[{"item": "rp", "value": "99999"}]`.
#### Disable registration
Set ```REGISTRATION``` in ```config.py``` to ```False```. Account can only be restored via UID after this is turned off.
@@ -97,6 +161,208 @@ clear the ```rank``` table of the database.
</details>
## Fanmade
`OverRide` uses `BMS` charting specification. Audio playbacks are overwritten (keysound will not work, and the sound files specified within the chart file is ignored). Channel 0, 1, 2 are the left 3 lanes, 3, 4 are scratch lanes for left and right, and 5, 6, 7 are the right 3 lanes. Speed changes can be specified using BMS's built in `bpm` channel, horizontal white lines are defined in channel 9.
Chart reversals require special flag `0B` and `0C` - the effect is enabled for two songs (`miserable` and `tkr`) on the official listing, and all charts on the fanmade listing.
Two ways to make charts:
1. Use OSU! Mania to make 8k or 6k charts and convert the ```.osu``` file to ```.bms```. The conversion tool is in the group file. Note: Use text editor to remove negative value ```TimingPoint``` from the ```.osu``` charts, or the tool will report error. After conversion, use a text editor to replace all ```ZZ``` to ```01```. After that, open the ```.bms``` chart with ```pBMsc``` to fix and modify. The chart should not contain non-01 value.
2. Use Malody to make 8k or 6k charts and convert the file to ```.osu```, then convert to `bms`. [code](https://github.com/Jakads/malody2osu)
// I will leave the actual charting to the professionals
You should have a ```mp3``` file and ```bms``` charts.
Make a ```id``` string for the song. Don't worry about collisions, as ```importer``` can address these. Find a thumbnail and name it ```id.jpg/png```.
Rename the charts to ```id_difficulty.bms```. Supported difficulties are ```EL,EX,PR,LPR,EL4,EX4,PR4```
Place charts to ```note``` folder. Place the thumbnail to ```thumbs``` folder. Place the music to ```music``` folder.
That's it for files. Next, let's modify ```manifest.json```.
```
[
{
"id": 1, // Ignore
"title": "Music Name", // Music title
"artist": "Music Artist", //Musician
"isJapanese": false, //Use Japanese display mode
"bpm": 167, //bpm number
"sync_6k": "0/0/0/-1400", //Chart offset values. 6k: EL,EX,LPR,PR. 4k: EL,EX,PR.
"sync_4k": "0/0/0",
"diff_6k": "0/0/0/18", // Chart difficulty。If not charted, use 0.
"diff_4k": "0/0/0",
"charter_6k": "-/-/-/charterA", //Charter information
"charter_4k": "-/-/-",
"mp3": "song", //Song ID.
"preview": "-1/-1", //Start time of 2 music previews, in seconds.
"bga": "-1400/24/3559" // Delete this line if no BGA is available
}
]
```
If you wish to make BGA, see section below. If not, delete the ```bga``` field.
That's it for the manifest. Select all files and folders, and compress to ```zip```. See "Adding Charts" section for chart importing.
#### Importer File Checking
`importer` will conduct basic check for the zip file. If the check is passed, only the necessary files will be copied to the server. 5 checks are conducted:
0. At least one chart should be defined in config.
Example: `diff_6k` and `diff_4k` not all zeroes.
1. A correctly named jpg or png should be in the thumbs folder.
Example: If the song ID is ```test```, a ```test.jpg``` or ```test.png``` should exist.
2. The correctly named mp3 should be in the music folder.
Example: If the song ID is ```test```, a ```test.mp3``` should exist.
3. If the manifest contains bga section, the bga zip file should be in the bga folder.
Example: If the song ID is ```test``` and has a ```bga``` section in manifest, a ```test.zip``` should exist.
4. If the manifest does not have bga, there should not be a bga zip file in the bga folder.
Example: If song ID ```test``` does not have ```bga``` section in manifest, ```test.zip``` should not exist. Note: Did you forget to add the bga to manifest?
5. All difficulties specified in the manifest should have a corresponding note file in the note folder.
Example: Song ID is ```test``` and manifest specifies multiple non-zero difficulties. All charts should exist.
### Making BGA
Download the MP4 from YouTube or other platforms. 360p is enough.
Put the video in the folder from the archive. Remember to ```pip install opencv-python```
Open ```cmd``` in the directory and type ```python v2b.py```
Follow the instructions. ```fps``` are typically 15 or 24. After the frames are extracted, go to the output folder and remove the black frame at the start and end to save some space.
ZIP all the images and fill the ```bga``` section of the manifest.
The 1st number is the millisecond offset. The 2nd number is the frame rate. The 3rd number is the frame number of the last frame.
Add the zip archive to the bga directory.
### Addendium for OverRide
In addition to the respective github repo, the tooling is also available in the `tools` folder. There is also this minimal client-server setup for fanmade charting specifically [Fanmade Server](https://github.com/qwerfd2/OverRapid-Fanmade-Server/)
That project is technically separate from Project OverRide, since that one is specifically designed for fanmade and carries some merit for that purpose (minimal server and client asset, essential assets are local, server only used for fanmade assets delivery, no official content), although it still requires a server to be set up (flask, not starlette).
Note: Although the fanmade chart package format is the exact same, the `importer.py` from the fanmade repo is NOT COMPATIBLE with Project OverRide. Project OverRide requires several additional considerations during implementation (collision with official listing and local assets, asset cleanup, compatibility with official manifest format), which means that although the tooling does the same thing, they way they achieved it is very different.
When importing a chart, you do not need to worry about:
1. The `mp3` value colliding with the official listing - everything is stored in either a different location or uses a different format;
2. Residual local assets if a fanmade song is deleted server-side - music, chart, and bga are automatically deleted if the manifest change is detected;
Note that the game client never had a way to customize per-song sync offset. That is done exclusively server-side, via `OverRide/playmanifest.json`.
`CNG`/`ARC`/`BRK` Difficulty is not supported for fanmade charts.
</details>
<details>
<summary>中文</summary>
<br>
## 为什么要OverRide
这个魔改带来了全新功能更新,内容,和bug修复。
如下是不全面的列表,描述了一些功能。
- 添加14首移除曲目 (Lag train, rainshower, happy?, cancer, chaotic, after white, overprocess, unfactual factory, euphoric planet, dawn, sudden noise, night ocean fantasy, Metatron rebirth, hymn for U)。
- 支持全部32个Arcane谱面。
- 添加3个移除的BGA (xmas17, cosmic, decryptor)。
- 添加2个移除的头像 (doge, xmas17)。
- 添加1个移除的皮肤 (xmas17)。
- 优化所有语言的本地化,支持简体中文。(优化是指把原版的文字图片替换成了代码,以获得清晰度并降低修改难度。日语和韩语是用Google翻译的 - 不是很理想)
- 可以直连私服,不需要中继软件。
- iOS, 安卓, 和 PC 支持。
- "隐形音符" 和加减速 (0.8x - 1.2x) 玩法。
- 添加了10个来自季节挑战的。。。挑战。
- 添加了 Outer Space 的BGE.
- 官方支持 Project OverWrite 和存档优化功能。
- 添加了Hadron Kollider的4k难度(超难),和H1V3的全难度特效支持。
- 重做解歌系统。曲包可用RP解锁,玩家等级解锁添加更多曲目。
- 优化游戏逻辑,删除无用代码,优化服务器和安装包资源。
- 支持通过UID恢复存档。移除社交媒体OAUTH功能。
- 支持通过FP控制台下载所有谱面和偏移值。
- 除了分数排行榜,文件下载,评级排行榜,和PVP,所有功能离线均可用。(必须先联网下载全部数据)
- 重做客户端登录流程。用户需要先起名才能下载资源。
- 可以关闭账号注册功能,也可以封禁用户。
- 移除歌曲文件加密和源码编译。
- 修复TKR皮肤的若干问题,透明度可通过BGA透明度自定义,调整Lanota BGE受用的曲目。
- TKR重制-快切换过去看看吧!
- 添加Render The Over Stage主题(老版本主题),支持全部新功能。
- 内置完备的自制谱支持。服务器导入,立刻在客户端单机或多人游玩。
- 修复Bug。
## 服务器
下载所有代码和资源,把资源解压到服务器根目录。
## 资源
在服务器可用前,有三个需要配置的文件夹。
都位于根目录下。
`OverRapid`: 包括音乐,谱面,BGA,和配置文件。稳定,可以版本更新。
`OverRide`: 自制内容的初始文件夹。不需要担心。
`Resource`: 包括基础素材,比如图像,视频,和音频。不稳定,可能出现很多变化。
如下Google Drive和百度网盘链接指向一个文件夹,包括两个zip文档。
根据需求,下载一个或全部并解压到服务器根目录。
如果这是你第一次配置服务器,全部下载。
如果你是来这里更新的,那么可以仅下载`Resources` `素材包2`。把老的`Resources`文件夹删掉,覆盖任何冲突内容。
V4 Asset: [Google Drive](https://drive.google.com/drive/folders/1jR37N2wi6M1l0Eq1QeO-UkVtXPd-vkJt?usp=sharing) [Baidu Drive](https://pan.baidu.com/s/1jhzKBP3zP0dnfgcsLS1xGQ Code: AAAA)
之后,跟随使用手册配置。
## 使用手册
<details>
@@ -105,14 +371,14 @@ clear the ```rank``` table of the database.
### 前言
如有错漏敬请加群(511974777)联系。不鼓励使用私服游玩官曲的行为。不会提供这方面的帮助。
如有错漏敬请加群(511974777)联系。欢迎使用Github Issues汇报服务器及客户端错误。
### 搭建服务器和连接
群文件下载服务器,下载对应平台的安装包。
群文件或Github Release下载服务器,下载对应平台的安装包。
PC 或 MAC(统称主机)解压服务器,linux 自己去搞(
PC 或 MAC(统称主机)解压服务器,~~linux 自己去搞(~~
设备安装下载好的安装包。 Android 已修改包名和图标,不会和官方冲突。iOS 需要 trollstore 之类的 jailbroken 工具。
@@ -146,6 +412,10 @@ PC 打开 ```cmd``` 输入 ```ipconfig```。MAC 打开 ```terminal``` 输入 ```
你也可以用```DB Browser```打开```player.db```来寻找```UID```。
#### 添加货币
目前,游戏给予无限货币。若以后发生改变,可以使用兑换码机制来添加货币。在数据库添加一个兑换码,`item`为`[{"item": "rp", "value": "99999"}]`。
#### 关闭注册
把```config.py```里的```REGISTRATION```设成```False```。如果关闭,只能通过UID恢复已有账号来开始游戏。
@@ -159,3 +429,116 @@ PC 打开 ```cmd``` 输入 ```ipconfig```。MAC 打开 ```terminal``` 输入 ```
清除```rank```数据库表。
</details>
## 自制
```OverRide``` 使用 ```BMS``` 谱面格式。音频播放不可用(按键音不可用,谱面文件内的音频被无视)。0, 1, 2 轨为左侧 3 轨,3, 4 为左右划键,5, 6, 7 为右侧 3 轨。变速可在谱面内通过`BPM`轨设定,水平白线通过9轨设定。
谱面倒退可以通过 ```0B``` 和 ```0C``` 来指定 - 官方列表只有两首可以使用(`miserable` 和 `tkr`),自制没有限制。
目前的两个制谱方式:
1. 通过 OSU! Mania 制 8k/6k 谱并将 ```.osu``` 文件转至 ```.bms```。转码工具在群文件里。注意事项:提前用文本编辑器删除负值 ```TimingPoint```,否则工具会报错。转码后用文本编辑器将所有 ```ZZ``` 变成 01. 之后用 ```pBMsc``` 打开 ```bms``` 进行修改修复并保存。谱面内不应出现非 01 的值。
2. 通过 Malody 制 8k/6k 谱并将谱面转至 ```.osu```, 再使用上述方法转至 `bms`. [代码](https://github.com/Jakads/malody2osu)
//具体制谱思路让专业的来写(
现在,你应该有歌曲的 ```mp3``` 音乐文件,和 ```bms``` 谱面。
给歌曲定个 ```id``` 字符串。不用担心和别人重名,如果使用 ```importer``` 导入会自动解决。找到歌曲的封面并重命名为 歌曲 ```id.jpg/png```.
将谱面重命名为 ```id_难度.bms```,支持的难度为 ```EL,EX,PR,LPR,EL4,EX4,PR4```.
谱面放到 ```note``` 文件夹。歌曲封面放到 ```thumbs```。音乐文件放到 ```music```。不要放无关的文件。至此,文件安放完毕。接下来要修改 ```manifest.json```.
```
[
{
"id": 1, // 无视
"title": "Music Name", // 曲名
"artist": "Music Artist", //曲师
"isJapanese": false, //是否用日语模式显示
"bpm": 167, //bpm 数字
"sync_6k": "0/0/0/-1400", //谱面偏移值. 6k: EL,EX,LPR,PR. 4k: EL,EX,PR.
"sync_4k": "0/0/0",
"diff_6k": "0/0/0/18", // 谱面难度。如果没有谱面填 0.
"diff_4k": "0/0/0",
"charter_6k": "-/-/-/charterA", //谱师信息
"charter_4k": "-/-/-",
"mp3": "song", //歌曲 ID。
"preview": "-1/-1", //两段音乐 preview 开始时间,以秒计
"bga": "-1400/24/3559" //如曲目没有BGA删除此行
}
]
```
如果想做 BGA 可以参考下方。如果不想做必须删除 ```bga``` 行。
至此,```manifest``` 编辑完成。全选所有文件夹和 ```manifest.json```,压缩成 ```zip```。
导入可以参考“添加谱面”章节。
### Importer 文件检查
```importer``` 会对包体进行基本检查。如果检查通过,只会将需要的文件拷贝到服务器里。总共检查 5 项内容:
0. 配置文件中至少要有一个谱面难度。
举例:`diff_4k`和`diff_6k`不能全是0.
1. 至少一个正确命名的 jpg 或 png 图片应该在 thumbs 文件夹里。
举例:如果歌曲 ID:```test```,```test.jpg``` 或者 ```test.png``` 应该存在。
2. 正确命名的 MP3 文件应该在 music 文件夹里。
举例:如果歌曲 ID:```test```,```test.mp3``` 应该存在。
3. 如果 manifest 里有 bga,则 bga 压缩包应该在 bga 文件夹里。
举例:如果歌曲 ID:```test``` 且 manifest 里有 ```bga``` 行,```test.zip``` 应该存在。
4. 如果 manifest 里没有 ```bga``` 而 ```bga``` 文件夹里存在 ```bga``` 压缩包。虽然不会出现错误程序也会提示。
举例:歌曲 ID:```test``` 且 manifest 里没有 ```bga``` 行,而 ```test.zip``` 存在。是不是忘写进 manifest 了?
5. 写进 manifest 的谱面难度应该都在 note 文件夹里。
举例:歌曲 ID:```test``` 且 manifest 里写明有多个非 0 难度,而 ```note``` 里缺少谱面。
### 制作 BGA
油管或者其他平台下载视频 MP4 文件。清晰度只要是 360p 以上就行。
将视频文件放到解压的文件夹里。记得 ```pip install opencv-python```
文件夹内 ```cmd``` 输入 ```python v2b.py```
根据提示操作,一般 ```fps``` 为 15 或 24. 完成后进入文件夹,删除黑屏的帧来节省空间。
所有图片压 zip 包,填写 manifest 里的 ```bga```。
第一个数字为毫秒偏移。第二个数字为帧率。第三个数字为最后一帧的数字。将压好的 zip 包放到 ```bga``` 文件夹里。
### OverRide附加内容
上面描述的工具既可以在他们自己的仓库下载,也可以在本仓库的`tools`文件夹找到。还有一个更袖珍的客户端-服务器方案,专为自制谱设计。[自制服务器](https://github.com/qwerfd2/OverRapid-Fanmade-Server/)
这个项目之所以和OverRide区分开,是因为它专为自制设计,有些独有的优势(最小化客户端和服务器资源,必要资源内置无需下载,服务器仅用作自制文件运输,无官方内容),尽管这个方案仍需要一个服务器(`flask`而不是`starlette`)
提示:尽管自制导入的文件完全一样,自制服务器的`importer.py`不可和此项目的`importer.py`混用。在这里,有一些特殊的考量(和官方列表和文件的冲突,资源清理,列表兼容),导致具体实施完全不同。
在导入时,不需要担心:
1. `mp3`和官方重叠 - 所有文件和配置都要么存在不同的地方,要么使用了不同的格式;
2. 服务器删曲后本地资源留存 - 当列表变化到达本地,音乐,谱面,和bga将被自动删除;
注意,客户端从没有具体谱面偏移调整保存的能力 - 这是服务器端功能,需要通过修改`OverRide/playmanifest.json`来实现。
自制不支持 `CNG`/`ARC`/`BRK` 难度。
</details>
+48
View File
@@ -0,0 +1,48 @@
import os
import hashlib
import json
def calculate_md5_and_size(file_path):
md5_hash = hashlib.md5()
file_size = 0
with open(file_path, "rb") as file:
for byte_block in iter(lambda: file.read(4096), b""):
md5_hash.update(byte_block)
file_size += len(byte_block)
return md5_hash.hexdigest(), file_size
def process_folder(folder_path):
md5_checksum_dict = {}
size_dict = {}
for root, dirs, files in os.walk(folder_path):
for file_name in files:
file_path = os.path.join(root, file_name)
relative_path = os.path.relpath(file_path, folder_path)
md5_checksum, file_size = calculate_md5_and_size(file_path)
md5_checksum_dict[relative_path.replace("\\", "/")] = md5_checksum
size_dict[relative_path.replace("\\", "/")] = file_size
return md5_checksum_dict, size_dict
def write_to_file(file_name, data):
with open(file_name, "w") as file:
file.write(json.dumps(data, separators=(',', ':')))
if __name__ == "__main__":
resources_folder = "Resources"
md5_checksum_dict, size_dict = process_folder(resources_folder)
md5_file_name = "OverRapid/Md5ListChecksum.json"
size_file_name = "OverRapid/SizeList.json"
write_to_file(md5_file_name, md5_checksum_dict)
write_to_file(size_file_name, size_dict)
print(f"MD5 checksums saved to {md5_file_name}")
print(f"File sizes saved to {size_file_name}")
+55
View File
@@ -0,0 +1,55 @@
import cv2
import os
video_file = input("Enter the name of the video file: 输入视频文件名称 ")
if not os.path.isfile(video_file) or not video_file.endswith('.mp4'):
print("The file does not exist or is not an MP4 file. 文件不存在或不是mp4")
exit()
id_string = input("Enter an ID string: 输入命名用ID")
frame_rate = float(input("Enter the frame rate: 输入帧率"))
output_dir = input("Enter the output directory: 输入保存文件夹目录")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
cap = cv2.VideoCapture(video_file)
video_frame_rate = cap.get(cv2.CAP_PROP_FPS)
if frame_rate > video_frame_rate:
frame_rate = video_frame_rate
original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
desired_width = 480
desired_height = 270
interval = int(video_frame_rate / frame_rate)
frame_count = 0
saved_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame.shape[1] != desired_width or frame.shape[0] != desired_height:
frame = cv2.resize(frame, (desired_width, desired_height))
if frame_count % interval == 0:
output_path = os.path.join(output_dir, f"{id_string}_{saved_count + 1}.jpg")
cv2.imwrite(output_path, frame)
saved_count += 1
frame_count += 1
cap.release()
print(f"Extracted {saved_count} frames and saved to {output_dir}.")
print(f"已提取 {saved_count} 帧并保存到了 {output_dir}.")
@@ -0,0 +1,18 @@
[
{
"id": 1,
"title": "Music Name",
"artist": "Music Artist",
"isJapanese": false,
"bpm": 167,
"sync_6k": "0/0/0/-1400",
"sync_4k": "0/0/0",
"diff_6k": "0/0/0/18",
"diff_4k": "0/0/0",
"charter_6k": "-/-/-/charterA",
"charter_4k": "-/-/-",
"mp3": "song",
"preview": "-1/-1",
"bga": "-1400/24/3559"
}
]
@@ -0,0 +1 @@
This is a test placeholder file
@@ -0,0 +1 @@
This is a test placeholder file
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.
+439
View File
@@ -0,0 +1,439 @@
import os
import json
import sys
import zipfile
from shutil import rmtree
from msvcrt import getch
import ctypes
import traceback
from datetime import datetime
lastfile = "N/A"
version = "Final"
date = "November 5th, 2019"
try:
ctypes.windll.kernel32.SetConsoleTitleW(f"Malody to osu!mania Converter v{version}") #https://stackoverflow.com/questions/7387276
if getattr(sys, 'frozen', False):
os.chdir(os.path.dirname(sys.executable))
#https://codeday.me/ko/qa/20190316/78831.html
print(f"Malody to osu!mania Converter v{version}")
print(date)
print("by Jakads\n\n")
def choose():
choice = getch().decode()
while choice not in 'yYnN':
choice = getch().decode()
if choice in 'nN':
return 0
else:
return 1
"""
if len(sys.argv) > 1 and sys.argv[1] == '--:update': #added ":" to disallow user to view this message by dragging in files
#checks the first condition first so it will not give IndexError
if os.path.isfile(sys.argv[2]):
os.remove(sys.argv[2])
else:
print(f'[X] Failed to remove the temporary batch file({sys.argv[2]}). Please remove it manually. This message is not supposed to be shown.\n')
print(f"[O] Successfully updated to v{version}! :D\n[!] Would you like to check out the changelog? (Y/N)\n")
if choose():
webbrowser.open('https://github.com/jakads/Malody-to-Osumania#changelog')
del sys.argv[1], sys.argv[1] #Deleting second element twice actually deletes second and third
print("(i) Checking for new updates . . .")
try:
latest = requests.get('https://github.com/jakads/Malody-to-Osumania/raw/master/version.txt')
latest.raise_for_status()
print(f"\n(i) Latest Version = v{latest.text}")
if latest.text != version:
print("\n[!] New update is available! Would you like to download? (Y/N)")
if not choose():
print("(i) Skipping the update.\n\n")
else:
print("(i) Downloading . . .")
exe = requests.get("https://github.com/jakads/Malody-to-Osumania/raw/master/convert.exe", stream=True)
exe.raise_for_status()
total = int(exe.headers.get('content-length'))
progress = tqdm(total=total, unit='B', unit_scale=True, unit_divisor=1024, ncols=80) #https://github.com/tqdm/tqdm/wiki/How-to-make-a-great-Progress-Bar
rand=''.join(random.choice(string.ascii_letters + string.digits) for i in range(16)) #https://pynative.com/python-generate-random-string/
filename=os.path.basename(sys.executable)
with open(f"{rand}.exe", 'wb') as f: #https://stackoverflow.com/questions/37573483
for chunk in exe.iter_content(chunk_size=8192):
if chunk:
progress.update(8192)
f.write(chunk)
with open(f'{rand}.bat', 'w', encoding='utf-8') as f:
f.write('\n'.join(['@echo off',
'echo (i) Restarting . . .',
"echo (i) All the files you've dragged in will be converted very soon!",
'timeout /t 5 /nobreak >nul',
f'del "{filename}"',
f'rename {rand}.exe "{filename}"',
'cls',
'"{0}" --:update {1}.bat {2}'.format(filename, rand, ' '.join(f'"{file}\"' for file in sys.argv[1:]) if len(sys.argv)>1 else '')])) #https://stackoverflow.com/questions/12007686
os.startfile(f'{rand}.bat')
sys.exit()
else:
print("\n[O] Your program is as good as new! We're good to go.\n\n")
except Exception as e:
print("\n[!] Error while connecting:", e)
print("\n[!] Connection to GitHub failed. Will just continue...\n\n")
"""
def recursive_file_gen(mydir):
for root, dirs, files in os.walk(mydir):
for file in files:
yield os.path.join(root, file)
#https://stackoverflow.com/questions/2865278
def convert(i, bgtmp, soundtmp):
lastfile = f'{i} (Crashed while reading)'
try:
with open(f'{i}',encoding='utf-8') as mc:
mcFile = json.loads(mc.read())
mcFile['meta']['mode']
except:
print(f"[!] FileWarning: {os.path.basename(i)} is not a valid .mc file. Ignoring...")
return 1
if not mcFile['meta']['mode'] == 0:
print(f"[!] KeyWarning: {os.path.basename(i)} is not a Key difficulty. Ignoring...")
return 1
ms = lambda beats, bpm, offset: 1000*(60/bpm)*beats+offset
beat = lambda beat: beat[0] + beat[1]/beat[2] #beats = [measure, nth beat, divisor]
col = lambda column, keys: int(512*(2*column+1)/(2*keys))
SVMap = False
line = mcFile['time']
meta = mcFile['meta']
note = mcFile['note']
if 'effect' in mcFile and len(mcFile['effect'])>0:
sv = mcFile['effect']
SVMap = True
soundnote = {}
keys = meta["mode_ext"]["column"]
for x in note:
if x.get('type',0):
soundnote = x
bpm = [line[0]['bpm']]
bpmoffset = [-soundnote.get('offset',0)]
if len(line)>1:
j=0
lastbeat=line[0]['beat']
for x in line[1:]:
bpm.append(x['bpm'])
bpmoffset.append(ms(beat(x['beat'])-beat(lastbeat),line[j]['bpm'],bpmoffset[j]))
j+=1
lastbeat=x['beat']
global title, artist #I know using global is a bad practice but c'mon
title = meta["song"]["title"]
artist = meta["song"]["artist"]
preview = meta.get('preview',-1)
titleorg = meta['song'].get('titleorg',title)
artistorg = meta['song'].get('artistorg',artist)
background = meta["background"]
if not background=="": bgtmp.append(f'{os.path.dirname(i)}\\{background}')
sound = soundnote["sound"]
if not sound=="": soundtmp.append(f'{os.path.dirname(i)}\\{sound}')
creator = meta["creator"]
version = meta["version"]
lastfile = f'{i} (Crashed while converting)'
with open(f'{os.path.splitext(i)[0]}.osu',mode='w',encoding='utf-8') as osu:
osuformat = ['osu file format v14',
'',
'[General]',
f'AudioFilename: {sound}',
'AudioLeadIn: 0',
f'PreviewTime: {preview}',
'Countdown: 0',
'SampleSet: Soft',
'StackLeniency: 0.7',
'Mode: 3',
'LetterboxInBreaks: 0',
'SpecialStyle: 0',
'WidescreenStoryboard: 0',
'',
'[Editor]',
'DistanceSpacing: 1.2',
'BeatDivisor: 4',
'GridSize: 8',
'TimelineZoom: 2.4',
'',
'[Metadata]',
f'Title:{title}',
f'TitleUnicode:{titleorg}',
f'Artist:{artist}',
f'ArtistUnicode:{artistorg}',
f'Creator:{creator}',
f'Version:{version}',
'Source:Malody',
'Tags:Malody Convert by Jakads',
'BeatmapID:0',
'BeatmapSetID:-1',
'',
'[Difficulty]',
'HPDrainRate:8',
f'CircleSize:{keys}',
'OverallDifficulty:8',
'ApproachRate:5',
'SliderMultiplier:1.4',
'SliderTickRate:1',
'',
'[Events]',
'//Background and Video events',
f'0,0,\"{background}\",0,0',
'',
'[TimingPoints]\n']
osu.write('\n'.join(osuformat))
#https://thrillfighter.tistory.com/310
bpmcount = len(bpm)
for x in range(bpmcount):
osu.write(f'{bpmoffset[x]},{60000/bpm[x]},{int(line[x].get("sign",4))},1,0,0,1,0\n')
if SVMap:
for n in sv:
j=0
for b in line:
if beat(b['beat']) > beat(n['beat']):
j+=1
else:
continue
j=bpmcount-j-1
if int(ms(beat(n["beat"]), bpm[j], bpmoffset[j])) >= bpmoffset[0]:
osu.write(f'{ms(beat(n["beat"])-beat(line[j]["beat"]), bpm[j], bpmoffset[j])},-{100/abs(n["scroll"]) if n["scroll"]!=0 else "1E+308"},{int(line[j].get("sign",4))},1,0,0,0,0\n')
osu.write('\n\n[HitObjects]')
for n in note:
if not n.get('type',0) == 0:
continue
j=0
k=0
for b in line:
if beat(b['beat']) > beat(n['beat']):
j+=1
else:
continue
if not n.get('endbeat') == None:
for b in line:
if beat(b['beat']) > beat(n['endbeat']):
k+=1
else:
continue
j=bpmcount-j-1
k=bpmcount-k-1
if int(ms(beat(n["beat"]), bpm[j], bpmoffset[j])) >= 0:
osu.write(f'\n{col(n["column"], keys)},192,{int(ms(beat(n["beat"])-beat(line[j]["beat"]), bpm[j], bpmoffset[j]))}')
if n.get('endbeat') == None: #Regular Note
osu.write(',1,0,0:0:0:')
else: #Long Note
osu.write(f',128,0,{int(ms(beat(n["endbeat"])-beat(line[k]["beat"]), bpm[k], bpmoffset[k]))}:0:0:0:')
if n.get('sound') == None:
osu.write('0:')
else: #Hitsound Note
osu.write('{0}:{1}'.format(n['vol'],n['sound'].replace('"','')))
return 0
def compress(compressname, name, bglist, soundlist):
osz = zipfile.ZipFile(f'{compressname}.osz','w')
for i in name:
lastfile = f'{i} (Crashed while compressing)'
osz.write(f'{os.path.splitext(i)[0]}.osu', f'{os.path.basename(i)}.osu')
os.remove(f'{os.path.splitext(i)[0]}.osu')
print(f'[O] Compressed: {os.path.splitext(i)[0]}.osu')
if not len(bglist)==0:
for i in bglist:
lastfile = f'{i} (Crashed while compressing)'
if os.path.isfile(i):
osz.write(i, os.path.basename(i))
print(f'[O] Compressed: {os.path.basename(i)}')
else:
print(f'[!] {os.path.basename(i)} is not found and thus not compressed.')
if not len(soundlist)==0:
for i in soundlist:
lastfile = f'{i} (Crashed while compressing)'
if os.path.isfile(i):
osz.write(i, os.path.basename(i))
print(f'[O] Compressed: {os.path.basename(i)}\n')
else:
print(f'[!] {os.path.basename(i)} is not found and thus not compressed.')
osz.close()
oszname.append(f'{compressname}.osz')
if len(sys.argv)<=1:
print("(i) Drag .mc or .mcz/.zip files into this program to convert them to .osu or .osz!")
print("(i) Press any key to exit.")
getch()
sys.exit()
MCDragged = False
MCValid = False
ZIPDragged = False
#FolderDragged = False
mcname = []
zipname = []
foldername = []
oszname = []
mctmp = []
print('(i) Checking file validity . . .\n')
for x in sys.argv[1:]:
lastfile = f'{x} (Crashed while checking)'
isMCZ = False
if os.path.isdir(x):
#zipname.append(os.path.splitext(x)[0])
#FolderDragged = True
print(f"[!] FileWarning: {os.path.basename(x)} is a directory, not a file. Ignoring...")
elif not os.path.isfile(x):
print(f"[!] FileWarning: {os.path.basename(x)} is not found. You normally aren't supposed to see this message. Ignoring...")
elif os.path.splitext(x)[1].lower() == ".mc":
mctmp.append(x)
MCDragged = True
elif os.path.splitext(x)[1].lower() in [".mcz", ".zip"]:
if os.path.splitext(x)[1].lower() == ".mcz":
isMCZ = True
os.rename(x, f'{os.path.splitext(x)[0]}.zip')
x = f'{os.path.splitext(x)[0]}.zip'
mcz = zipfile.ZipFile(x)
mcz.extractall(os.path.splitext(x)[0])
mcz.close()
if isMCZ:
os.rename(x, f'{os.path.splitext(x)[0]}.mcz')
zipname.append(os.path.splitext(x)[0])
ZIPDragged = True
else:
print(f"[!] FileWarning: The file type of {os.path.basename(x)} is not supported. Ignoring...")
if MCDragged:
mcname.append(mctmp)
if not MCDragged and not ZIPDragged:
#print("\n[X] FILEERROR: None of the files you've dragged in are supported. This program only accepts .mc, .mcz, .zip files, or folders with them.")
print("\n[X] FILEERROR: None of the files you've dragged in are supported. This program only accepts .mc, .mcz, or .zip files.")
print("(i) Press any key to exit.")
getch()
sys.exit()
title = ""
artist = ""
mctitle = ""
mcartist = ""
bglist = []
soundlist = []
print("\n\n(i) Converting . . .\n")
#Converting to .osu (dragged .mc files)
if MCDragged:
bgtmp = []
soundtmp = []
for i in mcname[0][:]: #https://stackoverflow.com/questions/7210578
if not convert(i, bgtmp, soundtmp):
print(f'[O] Converted: {os.path.basename(i)}')
MCValid = True
else:
mcname[0].remove(i)
mctitle = title
mcartist = artist
bglist.append(bgtmp)
soundlist.append(soundtmp)
#Converting to .osu (dragged .mcz/.zip files)
if ZIPDragged:
for folder in zipname:
print(f'\n\n(i) Converting {os.path.basename(folder)} . . .\n')
c=0
bgtmp = []
soundtmp = []
mctmp = []
filelist = list(recursive_file_gen(folder))
for files in filelist:
if os.path.splitext(files)[1] == ".mc":
if not convert(files, bgtmp, soundtmp):
print(f'[O] Converted: {os.path.basename(files)}')
c+=1
MCValid = True
mctmp.append(files)
if c>0:
foldername.append(folder)
bglist.append(bgtmp)
soundlist.append(soundtmp)
mcname.append(mctmp)
if not MCValid:
print("\n[X] FILEERROR: None of the files you've dragged are supported.")
print("(i) Press any key to exit.")
getch()
sys.exit()
print('\n\n(i) All the supported .mc files have been converted to .osu!\n(i) Either close the program now and move the files manually,\n(i) or press Enter to compress all into .osz.')
getch()
print('\n\n\n(i) Compressing . . .\n')
#Compress to .osz (dragged .mc files as single mapset)
if MCDragged:
compress(f'{mcartist} - {mctitle}', mcname[0], set(bglist[0]), set(soundlist[0]))
if ZIPDragged:
i = 1 if MCDragged else 0
for folder in zipname:
print(f'\n(i) Compressing {os.path.basename(folder)} . . .\n')
compress(f'{os.path.basename(folder)}', mcname[i], set(bglist[i]), set(soundlist[i]))
i+=1
rmtree(folder)
print('\n(i) The following .osz files have been created! Run the files to add the maps to osu! automatically.\n')
for i in oszname:
print(f'* {i}')
print('\n(i) Press any key to exit.')
getch()
except Exception as e:
print(f'\n\n\n[X] FATAL ERROR : {repr(e)}\n')
traceback.print_exc()
crashlog = f'CrashLog_{datetime.now().strftime("%Y%m%d%H%M%S")}.log'
with open(crashlog,mode='w',encoding='utf-8') as crash:
crash.write('lol rip xd\n\n')
crash.write(f"Target File: {lastfile}\n")
crash.write("If you would like to tell the dev about this issue, please attach the file above if available (and the whole folder too if possible) with this crash report.\n")
crash.write('DO NOT EDIT ANYTHING WRITTEN HERE.\n\n')
crash.write(traceback.format_exc())
print(f'\n[X] The crash log has been saved as {crashlog}.')
print('[X] Please tell the dev about this!')
print('(i) Press any key to exit.')
getch()
exit()
+5
View File
@@ -0,0 +1,5 @@
bga maker: instruction in main readme. Just follow the prompt.
malody to osu: open .mcz as zip, drag .mc chart file on to convert.exe. An osu file will be saved in the directory.
osu to bms: run `python osu.py -i *input .osu file path*`. A .bms file will be saved in the directory. Note the name of the file might change as it is defined internally in the input file.