EX)
and
https://discuss.eroscripts.com/t/conversation-heart-suggested-music/288486
OFS GIF Custom Simulator
This is a MultiFunPlayer, WebSocket-based GIF Custom Simulator
HOW TO
download file
Unzip it
- Connect MFP with OFS
- In Output, add WebSocket +
- Set the address to localhost and choose any port you like
(Turn off all axes except “stroke / up–down”)
- Click the clock icon in the top-right corner and select FixedUpdate,
then set the Hz to the maximum - Choose any port number for WebSocket → When launching GIF Custom Simulator.exe, enter that same port
For the view.gif file:

- The current view.gif is a 21-frame GIF that goes Up → Down (100 → 0) once
- The file must be in the same folder as GIF Custom Simulator.exe for it to load properly
If you want to use your own GIF:
→ Create a 21-frame GIF that goes Up → Down (one full cycle),
rename it to view.gif, and replace the existing file.
You can edit or create the GIF easily using tools like ezgif.
P.S
It was developed in Rust
I built this quickly using GPT, so I didn’t add any extra features (like support for 11-frame GIFs, etc.). And I don’t really know much about coding…
When loading a script, the first 3–5 seconds may be slightly out of sync — same thing if you manually jump to a different section.
I’m not even fully sure how reliably it works yet.
The script likely places one frame per 5% interval, so sections that use fine adjustments like 98, 93, and so on might not display correctly.
The vibration sections with a 33ms interval seem to be working fine, though.
Please change the code as much as you want
Source Code
main.rs
use std::{
fs::File,
io::{self, BufReader, Write},
net::TcpListener,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
thread,
time::Duration,
};
use anyhow::Result;
use image::codecs::gif::GifDecoder;
use image::AnimationDecoder;
use minifb::{Key, Window, WindowOptions};
use tungstenite::{accept, Message};
type SharedIndex = Arc<AtomicUsize>;
fn main() -> Result<()> {
// ------------------------
// 🔷 port input in console
// ------------------------
print!("WebSocket port is (ex: 8090) : ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let port: u16 = input.trim().parse().unwrap_or(8090);
let ws_addr = format!("127.0.0.1:{port}");
println!("[WS] select port = {port}");
// ------------------------
// GIF loading
// ------------------------
let gif_path = "view.gif"; // ⚡ read same Path in exe
let max_frames = 21;
let (width, height, frames) = load_gif_frames(gif_path, max_frames)?;
println!("GIF loaded: {width}x{height}, {} frames", frames.len());
// ------------------------
// mapping generate
// ------------------------
let mapping = Arc::new(build_mapping(frames.len()));
// WebSocket & rendering share state
let latest_idx: SharedIndex = Arc::new(AtomicUsize::new(0));
// ------------------------
// WebSocket thread start
// ------------------------
{
let idx_ws = Arc::clone(&latest_idx);
let map_ws = Arc::clone(&mapping);
thread::spawn(move || {
if let Err(e) = run_ws_server_custom(idx_ws, map_ws, ws_addr) {
eprintln!("[WS] error: {e:?}");
}
});
}
// ------------------------
// screen rendering start
// ------------------------
run_window(width, height, frames, latest_idx)?;
Ok(())
}
// -----------------------------------
// GIF read
// -----------------------------------
fn load_gif_frames(path: &str, max_frames: usize) -> Result<(usize, usize, Vec<Vec<u32>>)> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let decoder = GifDecoder::new(reader)?;
let frames = decoder.into_frames();
let frames = frames.collect_frames()?;
let mut out_frames = Vec::new();
let mut width = 0;
let mut height = 0;
for frame in frames.into_iter().take(max_frames) {
let buf = frame.buffer();
width = buf.width() as usize;
height = buf.height() as usize;
let rgba = buf.as_raw();
let mut rgb32 = vec![0u32; width * height];
for i in 0..(width * height) {
let r = rgba[i * 4] as u32;
let g = rgba[i * 4 + 1] as u32;
let b = rgba[i * 4 + 2] as u32;
rgb32[i] = (r << 16) | (g << 8) | b;
}
out_frames.push(rgb32);
}
Ok((width, height, out_frames))
}
// -----------------------------
// mapping 0~10000 → frame idx
// -----------------------------
fn build_mapping(frame_len: usize) -> Vec<usize> {
let mut map = Vec::with_capacity(10001);
for x in 0..=10000 {
let idx = (frame_len - 1) - ((x * frame_len) / 10000);
map.push(idx);
}
map
}
// -----------------------------
// WS server
// -----------------------------
fn run_ws_server_custom(
latest: SharedIndex,
mapping: Arc<Vec<usize>>,
addr: String,
) -> Result<()> {
let server = TcpListener::bind(&addr)?;
println!("[WS] listening on ws://{addr}");
for stream in server.incoming() {
let stream = stream?;
let idx_ws = Arc::clone(&latest);
let map_ws = Arc::clone(&mapping);
thread::spawn(move || {
if let Err(e) = handle_client(stream, idx_ws, map_ws) {
eprintln!("[WS client] error: {e:?}");
}
});
}
Ok(())
}
fn handle_client(
stream: std::net::TcpStream,
latest: SharedIndex,
mapping: Arc<Vec<usize>>,
) -> Result<()> {
let mut ws = accept(stream)?;
loop {
let msg = ws.read_message()?;
match msg {
Message::Text(text) => {
if text.len() >= 6 {
if let Some(v) = parse_4digits(&text[2..6]) {
let v = v as usize;
if v < mapping.len() {
let idx = mapping[v];
latest.store(idx, Ordering::Relaxed);
}
}
}
}
Message::Close(_) => break,
_ => {}
}
}
Ok(())
}
// -----------------------------
// -----------------------------
fn parse_4digits(s: &str) -> Option<u16> {
if s.len() != 4 {
return None;
}
let b = s.as_bytes();
let d0 = b[0].wrapping_sub(b'0') as u16;
let d1 = b[1].wrapping_sub(b'0') as u16;
let d2 = b[2].wrapping_sub(b'0') as u16;
let d3 = b[3].wrapping_sub(b'0') as u16;
if d0 > 9 || d1 > 9 || d2 > 9 || d3 > 9 {
return None;
}
Some(d0 * 1000 + d1 * 100 + d2 * 10 + d3)
}
// -----------------------------
// -----------------------------
fn run_window(
width: usize,
height: usize,
frames: Vec<Vec<u32>>,
latest: SharedIndex,
) -> Result<()> {
let mut window = Window::new(
"GIF Custom Simulator (99DM, ananymous) ",
width,
height,
WindowOptions::default(),
)?;
// CPU
window.limit_update_rate(Some(Duration::from_micros(200)));
while window.is_open() && !window.is_key_down(Key::Escape) {
let idx = latest.load(Ordering::Relaxed).min(frames.len() - 1);
window.update_with_buffer(&frames[idx], width, height)?;
}
Ok(())
}



