1
0

chore: change project layout

This commit is contained in:
2026-01-19 10:44:10 +08:00
parent 0e9837a75a
commit 5323617ca6
9 changed files with 133 additions and 5 deletions

View File

@@ -0,0 +1,351 @@
use pyo3::prelude::*;
pub(crate) mod wrapped;
#[pymodule(name = "_blctas")]
/// Provides functionality for handling Ballance TAS works.
mod blctas {
#[pymodule_export]
use super::tasfile;
}
#[pymodule(submodule)]
/// Provides functionality for handling Ballance TAS files loading, saving and editing.
mod tasfile {
use pyo3::{exceptions::PyRuntimeError, prelude::*};
use crate::wrapped::tasfile::{
Error as RsTasError, TasFile as RsTasFile, TasFrame as RsTasFrame, TasKey as RsTasKey
};
impl From<RsTasError> for PyErr {
fn from(error: RsTasError) -> Self {
PyRuntimeError::new_err(error.to_string())
}
}
#[pyclass(eq, eq_int)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// Represents the different keys that can be pressed in a TAS frame.
enum TasKey {
#[pyo3(name = "KEY_UP")]
/// Up arrow key
KeyUp,
#[pyo3(name = "KEY_DOWN")]
/// Down arrow key
KeyDown,
#[pyo3(name = "KEY_LEFT")]
/// Left arrow key
KeyLeft,
#[pyo3(name = "KEY_RIGHT")]
/// Right arrow key
KeyRight,
#[pyo3(name = "KEY_SHIFT")]
/// Shift key
KeyShift,
#[pyo3(name = "KEY_SPACE")]
/// Spacebar key
KeySpace,
#[pyo3(name = "KEY_Q")]
/// Q key
KeyQ,
#[pyo3(name = "KEY_ESC")]
/// Escape key
KeyEsc,
#[pyo3(name = "KEY_ENTER")]
/// Enter key
KeyEnter,
}
impl From<TasKey> for RsTasKey {
fn from(key: TasKey) -> RsTasKey {
match key {
TasKey::KeyUp => RsTasKey::KeyUp,
TasKey::KeyDown => RsTasKey::KeyDown,
TasKey::KeyLeft => RsTasKey::KeyLeft,
TasKey::KeyRight => RsTasKey::KeyRight,
TasKey::KeyShift => RsTasKey::KeyShift,
TasKey::KeySpace => RsTasKey::KeySpace,
TasKey::KeyQ => RsTasKey::KeyQ,
TasKey::KeyEsc => RsTasKey::KeyEsc,
TasKey::KeyEnter => RsTasKey::KeyEnter,
}
}
}
#[pyclass]
#[derive(Debug)]
/// Represents a TAS file containing a sequence of frames.
/// Each frame contains key press information and delta time for Ballance gameplay.
struct TasFile {
inner: RsTasFile
}
#[pymethods]
impl TasFile {
// region: Status
/// Clears all frames from the TAS file.
fn clear(&mut self) -> PyResult<()> {
Ok(self.inner.clear())
}
/// Gets the number of frames in the TAS file.
///
/// Returns:
/// The count of frames in the TAS file.
fn get_count(&self) -> PyResult<usize> {
Ok(self.inner.get_count())
}
/// Checks if the TAS file is empty (contains no frames).
///
/// Returns:
/// True if the TAS file is empty, False otherwise.
fn is_empty(&self) -> PyResult<bool> {
Ok(self.inner.is_empty())
}
// endregion
// region: Single Operation
/// Gets the delta time of a frame at the specified index.
///
/// Args:
/// index: The index of the frame to get the delta time from.
///
/// Returns:
/// The delta time value of the frame at the specified index.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn get_delta_time(&self, index: usize) -> PyResult<f32> {
Ok(self.inner.visit(index)?.get_delta_time())
}
/// Sets the delta time of a frame at the specified index.
///
/// Args:
/// index: The index of the frame to set the delta time for.
/// delta_time: The new delta time value to set.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn set_delta_time(&mut self, index: usize, delta_time: f32) -> PyResult<()> {
Ok(self.inner.visit_mut(index)?.set_delta_time(delta_time))
}
/// Checks if a specific key is pressed in the frame at the specified index.
///
/// Args:
/// index: The index of the frame to check.
/// key: The key to check for press status.
///
/// Returns:
/// True if the key is pressed, False otherwise.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn is_key_pressed(&self, index: usize, key: TasKey) -> PyResult<bool> {
Ok(self.inner.visit(index)?.is_key_pressed(key.into()))
}
/// Sets the press status of a specific key in the frame at the specified index.
///
/// Args:
/// index: The index of the frame to modify.
/// key: The key to set the press status for.
/// pressed: True to press the key, False to release it.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn set_key_pressed(&mut self, index: usize, key: TasKey, pressed: bool) -> PyResult<()> {
Ok(self.inner.visit_mut(index)?.set_key_pressed(key.into(), pressed))
}
/// Flips the press status of a specific key in the frame at the specified index.
/// If the key was pressed, it becomes released; if it was released, it becomes pressed.
///
/// Args:
/// index: The index of the frame to modify.
/// key: The key to flip the press status for.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn flip_key_pressed(&mut self, index: usize, key: TasKey) -> PyResult<()> {
Ok(self.inner.visit_mut(index)?.flip_key_pressed(key.into()))
}
/// Clears all key presses in the frame at the specified index, setting all keys to released state.
///
/// Args:
/// index: The index of the frame to clear key presses for.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn clear_key_pressed(&mut self, index: usize) -> PyResult<()> {
Ok(self.inner.visit_mut(index)?.clear_key_pressed())
}
// endregion
// region: Batchly Operation
/// Sets the delta time for a range of frames from index_from to index_to (inclusive).
/// This is a batch operation that modifies multiple frames in one call, which is more
/// efficient than calling set_delta_time individually on each frame.
///
/// Args:
/// index_from: The starting index (inclusive) of the range to modify.
/// index_to: The ending index (inclusive) of the range to modify.
/// delta_time: The new delta time value to set for all frames in the range.
///
/// Raises:
/// RuntimeError: If either index is out of range or if index_to < index_from.
fn batchly_set_delta_time(&mut self, index_from: usize, index_to: usize, delta_time: f32) -> PyResult<()> {
for frame in self.inner.batchly_visit_mut(index_from, index_to)? {
frame.set_delta_time(delta_time);
}
Ok(())
}
/// Sets the press status of a specific key for a range of frames from index_from to index_to (inclusive).
/// This is a batch operation that modifies multiple frames in one call, which is more
/// efficient than calling set_key_pressed individually on each frame.
///
/// Args:
/// index_from: The starting index (inclusive) of the range to modify.
/// index_to: The ending index (inclusive) of the range to modify.
/// key: The key to set the press status for.
/// pressed: True to press the key, False to release it for all frames in the range.
///
/// Raises:
/// RuntimeError: If either index is out of range or if index_to < index_from.
fn batchly_set_key_pressed(&mut self, index_from: usize, index_to: usize, key: TasKey, pressed: bool) -> PyResult<()> {
for frame in self.inner.batchly_visit_mut(index_from, index_to)? {
frame.set_key_pressed(key.into(), pressed);
}
Ok(())
}
/// Flips the press status of a specific key for a range of frames from index_from to index_to (inclusive).
/// This is a batch operation that modifies multiple frames in one call, which is more
/// efficient than calling flip_key_pressed individually on each frame.
///
/// Args:
/// index_from: The starting index (inclusive) of the range to modify.
/// index_to: The ending index (inclusive) of the range to modify.
/// key: The key to flip the press status for in all frames in the range.
///
/// Raises:
/// RuntimeError: If either index is out of range or if index_to < index_from.
fn batchly_flip_key_pressed(&mut self, index_from: usize, index_to: usize, key: TasKey) -> PyResult<()> {
for frame in self.inner.batchly_visit_mut(index_from, index_to)? {
frame.flip_key_pressed(key.into());
}
Ok(())
}
/// Clears all key presses for a range of frames from index_from to index_to (inclusive).
/// This sets all keys to the released state for all frames in the range.
/// This is a batch operation that modifies multiple frames in one call, which is more
/// efficient than calling clear_key_pressed individually on each frame.
///
/// Args:
/// index_from: The starting index (inclusive) of the range to modify.
/// index_to: The ending index (inclusive) of the range to modify.
///
/// Raises:
/// RuntimeError: If either index is out of range or if index_to < index_from.
fn batchly_clear_key_pressed(&mut self, index_from: usize, index_to: usize) -> PyResult<()> {
for frame in self.inner.batchly_visit_mut(index_from, index_to)? {
frame.clear_key_pressed();
}
Ok(())
}
// endregion
// region: Modify
/// Appends a specified number of frames with the given delta time to the end of the TAS file.
///
/// Args:
/// count: The number of frames to append.
/// delta_time: The delta time value for the new frames.
fn append(&mut self, count: usize, delta_time: f32) -> PyResult<()> {
let frames = vec![RsTasFrame::with_delta_time(delta_time); count];
Ok(self.inner.append(&frames))
}
/// Inserts a specified number of frames with the given delta time at the specified index.
///
/// Args:
/// index: The position at which to insert the new frames.
/// count: The number of frames to insert.
/// delta_time: The delta time value for the new frames.
///
/// Raises:
/// RuntimeError: If the index is out of range.
fn insert(&mut self, index: usize, count: usize, delta_time: f32) -> PyResult<()> {
let frames = vec![RsTasFrame::with_delta_time(delta_time); count];
Ok(self.inner.insert(index, &frames)?)
}
/// Removes frames from the TAS file within the specified range (inclusive).
///
/// Args:
/// index_from: The starting index (inclusive) of the range to remove.
/// index_to: The ending index (inclusive) of the range to remove.
///
/// Raises:
/// RuntimeError: If either index is out of range or if index_to < index_from.
fn remove(&mut self, index_from: usize, index_to: usize) -> PyResult<()> {
Ok(self.inner.remove(index_from, index_to)?)
}
// endregion
}
#[pyfunction]
/// Creates a new TAS file with a specified number of frames, all having the same delta time.
///
/// Args:
/// count: The number of frames to create in the new TAS file.
/// delta_time: The delta time value for all frames in the new TAS file.
///
/// Returns:
/// A new TasFile instance with the specified number of frames.
fn create(count: usize, delta_time: f32) -> PyResult<TasFile> {
Ok(TasFile { inner: RsTasFile::new(vec![RsTasFrame::with_delta_time(delta_time); count]) })
}
#[pyfunction]
/// Loads a TAS file from disk.
///
/// Args:
/// filename: The path to the TAS file to load.
///
/// Returns:
/// A TasFile instance loaded from the specified file.
///
/// Raises:
/// RuntimeError: If the file cannot be loaded or is invalid.
fn load(filename: &str) -> PyResult<TasFile> {
Ok(TasFile { inner: RsTasFile::load(filename)? })
}
#[pyfunction]
/// Saves a TAS file to disk.
///
/// Args:
/// file: The TasFile instance to save.
/// filename: The path where the TAS file should be saved.
///
/// Raises:
/// RuntimeError: If the file cannot be saved.
fn save(file: &TasFile, filename: &str) -> PyResult<()> {
Ok(file.inner.save(filename)?)
}
}

View File

@@ -0,0 +1 @@
pub(crate) mod tasfile;

View File

@@ -0,0 +1,337 @@
use byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
use libz_sys;
use std::ffi::{c_int, c_ulong};
use std::fs::File;
use std::io::{Read, Write};
use std::mem::MaybeUninit;
use thiserror::Error as TeError;
#[derive(Debug, TeError)]
pub enum Error {
#[error("given index is out of range")]
IndexOutOfRange,
#[error("arithmetic overflow")]
NumOverflow,
#[error("fail to cast numeric value")]
BadNumCast,
#[error("fail to read or write file")]
Io(#[from] std::io::Error),
#[error("fail to call zlib function")]
ZlibCall,
#[error("given TAS file is wrong")]
BadTasFile,
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TasKey {
KeyUp,
KeyDown,
KeyLeft,
KeyRight,
KeyShift,
KeySpace,
KeyQ,
KeyEsc,
KeyEnter,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
#[repr(C)]
pub struct TasFrame {
delta_time: f32,
key_flags: u32,
}
impl TasFrame {
pub fn new(delta_time: f32, key_flags: u32) -> Self {
Self {
delta_time,
key_flags,
}
}
pub fn with_delta_time(delta_time: f32) -> Self {
Self::new(delta_time, 0u32)
}
}
impl TasFrame {
pub fn get_delta_time(&self) -> f32 {
self.delta_time
}
pub fn set_delta_time(&mut self, delta_time: f32) -> () {
self.delta_time = delta_time
}
}
impl TasFrame {
fn get_key_flag(key: TasKey) -> u32 {
let bit = match key {
TasKey::KeyUp => 0,
TasKey::KeyDown => 1,
TasKey::KeyLeft => 2,
TasKey::KeyRight => 3,
TasKey::KeyShift => 4,
TasKey::KeySpace => 5,
TasKey::KeyQ => 6,
TasKey::KeyEsc => 7,
TasKey::KeyEnter => 8,
};
1u32 << bit
}
pub fn is_key_pressed(&self, key: TasKey) -> bool {
(self.key_flags & Self::get_key_flag(key)) != 0u32
}
pub fn set_key_pressed(&mut self, key: TasKey, pressed: bool) {
if pressed {
self.key_flags |= Self::get_key_flag(key)
} else {
self.key_flags &= !(Self::get_key_flag(key))
}
}
pub fn flip_key_pressed(&mut self, key: TasKey) {
self.key_flags ^= Self::get_key_flag(key)
}
pub fn clear_key_pressed(&mut self) {
self.key_flags = 0
}
}
#[derive(Debug)]
pub struct TasFile {
frames: Vec<TasFrame>,
}
impl TasFile {
pub fn new(frames: Vec<TasFrame>) -> Self {
Self { frames }
}
pub fn load(filename: &str) -> Result<Self> {
// Open file
let mut reader = File::open(filename)?;
// Read decompressed size.
let u32_decomp_size = reader.read_u32::<NativeEndian>()?;
let usize_decomp_size = usize::try_from(u32_decomp_size).map_err(|_| Error::BadNumCast)?;
let culong_decomp_size =
c_ulong::try_from(usize_decomp_size).map_err(|_| Error::BadNumCast)?;
// Check size and compute frame count.
let frame_size = size_of::<TasFrame>();
let frame_count = usize_decomp_size
.checked_div(frame_size)
.ok_or(Error::NumOverflow)?;
if !usize_decomp_size.is_multiple_of(frame_size) {
return Err(Error::BadTasFile);
}
// Read all rest file into memory
let mut comp_buffer = Vec::new();
reader.read_to_end(&mut comp_buffer)?;
// Get compressed buffer size.
let usize_comp_size = comp_buffer.len();
let culong_comp_size = c_ulong::try_from(usize_comp_size).map_err(|_| Error::BadNumCast)?;
// Create decompressed buffer with uninitialized memory
let mut decomp_buffer: Box<[MaybeUninit<TasFrame>]> = Box::new_uninit_slice(frame_count);
// Decompress data
let source = comp_buffer.as_ptr() as *const libz_sys::Bytef;
let source_len = culong_comp_size;
let dest = decomp_buffer.as_mut_ptr() as *mut libz_sys::Bytef;
let mut dest_len = culong_decomp_size;
let rv = unsafe { libz_sys::uncompress(dest, &mut dest_len, source, source_len) };
if rv != libz_sys::Z_OK {
return Err(Error::ZlibCall);
}
// Convert uninitialized buffer to initialized
// SAFETY: We've just initialized the buffer with uncompress
let frames = unsafe {
std::slice::from_raw_parts(decomp_buffer.as_ptr() as *const TasFrame, frame_count)
.to_vec()
};
// Okey
Ok(Self::new(frames))
}
pub fn save(&self, filename: &str) -> Result<()> {
// Open file
let mut writer = File::create(filename)?;
// Get decompressed size.
let usize_decomp_size = size_of::<TasFrame>()
.checked_mul(self.frames.len())
.ok_or(Error::NumOverflow)?;
// Write decompressed size.
let u32_decomp_size = u32::try_from(usize_decomp_size).map_err(|_| Error::BadNumCast)?;
writer.write_u32::<NativeEndian>(u32_decomp_size)?;
// Get compressed buffer boundary
let culong_decomp_size =
c_ulong::try_from(usize_decomp_size).map_err(|_| Error::BadNumCast)?;
let culong_comp_bound_size: c_ulong =
unsafe { libz_sys::compressBound(culong_decomp_size) };
// Create buffer for it.
let usize_comp_bound_size =
usize::try_from(culong_comp_bound_size).map_err(|_| Error::BadNumCast)?;
let mut buffer: Box<[MaybeUninit<u8>]> = Box::new_uninit_slice(usize_comp_bound_size);
// Compress data into buffer
let source = self.frames.as_ptr() as *const libz_sys::Bytef;
let source_len: c_ulong = culong_decomp_size;
let dest = buffer.as_mut_ptr() as *mut libz_sys::Bytef;
let mut dest_len: c_ulong = culong_comp_bound_size;
let level: c_int = 9;
let rv = unsafe { libz_sys::compress2(dest, &mut dest_len, source, source_len, level) };
if rv != libz_sys::Z_OK {
return Err(Error::ZlibCall);
}
// Fetch the final compressed length.
let culong_comp_size: c_ulong = dest_len;
let usize_comp_size: usize =
usize::try_from(culong_comp_size).map_err(|_| Error::BadNumCast)?;
// Write compressed data
let buffer = unsafe { buffer.assume_init() };
writer.write(&buffer[..usize_comp_size])?;
// Okey
Ok(())
}
}
impl TasFile {
/// 清空存储结构。
pub fn clear(&mut self) {
self.frames.clear()
}
/// 获取当前存储的TAS帧的个数。
pub fn get_count(&self) -> usize {
self.frames.len()
}
/// 获取当前存储结构是不是空的。
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
}
impl TasFile {
fn check_index(&self, index: usize) -> bool {
index < self.frames.len()
}
fn check_index_range(&self, index_from: usize, index_to: usize) -> bool {
// Check index relation
if index_to < index_from {
return false;
}
// Check index range
if index_to >= self.frames.len() {
return false;
}
// Okey
return true;
}
/// 访问给定索引的帧。
pub fn visit<'a>(&'a self, index: usize) -> Result<&'a TasFrame> {
if self.check_index(index) {
Ok(&self.frames[index])
} else {
Err(Error::IndexOutOfRange)
}
}
/// 以可变形式访问给定索引的值。
pub fn visit_mut<'a>(&'a mut self, index: usize) -> Result<&'a mut TasFrame> {
if self.check_index(index) {
Ok(&mut self.frames[index])
} else {
Err(Error::IndexOutOfRange)
}
}
/// 访问给定索引范围内的帧。
pub fn batchly_visit<'a>(&'a self, index_from: usize, index_to: usize) -> Result<&'a [TasFrame]> {
if self.check_index_range(index_from, index_to) {
Ok(&self.frames[index_from..=index_to])
} else {
Err(Error::IndexOutOfRange)
}
}
/// 以可变形式访问给定索引范围内的帧。
pub fn batchly_visit_mut<'a>(&'a mut self, index_from: usize, index_to: usize) -> Result<&'a mut[TasFrame]> {
if self.check_index_range(index_from, index_to) {
Ok(&mut self.frames[index_from..=index_to])
} else {
Err(Error::IndexOutOfRange)
}
}
}
impl TasFile {
/// 在结尾继续添加给的的帧序列。
///
/// `frames`为要插入的元素的切片。
pub fn append(&mut self, frames: &[TasFrame]) {
self.frames.extend_from_slice(frames)
}
/// 在给定的索引**之前**插入给定的帧序列。
///
/// 按照此函数约定如果要在头部插入数据则可以通过指定0来实现。
/// 然而对于在尾部插入数据,或在空的存储中插入数据,可以指定存储结构的长度来实现。
/// 即指定最大Index + 1的值来实现。
///
/// `index`为要在前方插入数据的元素的索引。`frames`为要插入的元素的切片。
pub fn insert(&mut self, index: usize, frames: &[TasFrame]) -> Result<()> {
if index > self.frames.len() {
Err(Error::IndexOutOfRange)
} else if index == self.frames.len() {
// Insert at tail
self.frames.extend_from_slice(frames);
Ok(())
} else {
// Insert at middle or head
self.frames.splice(index..index, frames.iter().copied());
Ok(())
}
}
/// 将给定范围内的帧移除。
///
/// `index_from`为要开始移除的单元的索引。`index_to`为最后一个要移除的单元的索引。
/// `index_from`和`index_to`指向的帧均会被删除端点inclusive模式
/// `index_to`不能小于`index_from`。
/// `index_from`和`index_to`均不能超过最大索引。
pub fn remove(&mut self, index_from: usize, index_to: usize) -> Result<()> {
// Check index relation
if index_to < index_from {
return Err(Error::IndexOutOfRange);
}
// Check index range
if index_to >= self.frames.len() {
return Err(Error::IndexOutOfRange);
}
// Perform remove
self.frames.drain(index_from..=index_to);
Ok(())
}
}