/* wearechat - generic multiplayer game with voip Copyright (C) 2025 metamuffin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ #![feature(iter_array_chunks)] #![allow(clippy::too_many_arguments, clippy::type_complexity)] pub mod animation; pub mod mesh; pub mod physics; pub mod prefab; pub mod vrm; use anyhow::{Context, Result, anyhow, bail}; use clap::Parser; use gltf::{image::Source, scene::Transform}; use humansize::BINARY; use image::{ImageReader, codecs::webp::WebPEncoder}; use log::{debug, info}; use prefab::import_prefab; use rand::random; use std::{ borrow::Cow, collections::HashMap, fs::File, io::{BufWriter, Cursor, Read, Write}, marker::PhantomData, net::{SocketAddr, TcpStream}, path::{Path, PathBuf}, sync::{Arc, Mutex}, thread::{self, sleep}, time::Duration, }; use weareshared::{ Affine3A, Vec3A, helper::ReadWrite, packets::{Data, Object, Packet, Resource}, resources::{Image, RespackEntry}, respack::save_respack, store::ResourceStore, vec3a, }; #[derive(Parser)] pub struct Args { #[arg(short, long)] address: Option, /// Output converted prefab as resource package #[arg(short = 'o', long)] pack: Option, /// Path(s) to a glTF file, binary or json format scene: Vec, /// Send all resources to the server then quit #[arg(short, long)] push: bool, /// Remove all other object from the world #[arg(short, long)] clear: bool, /// Add the object to the world #[arg(short, long)] add: bool, /// Transcode all textures to WebP #[arg(short, long)] webp: bool, /// Add skybox #[arg(long)] skybox: Option, /// Override prefab name #[arg(short, long)] name: Option, #[arg(short, long)] scale: Option, #[arg(short, long)] dry_run: bool, #[arg(short, long)] line_up: bool, #[arg(long)] use_cache: bool, #[arg(short = 'S', long)] with_default_sun: bool, #[arg(long)] animation: Option, #[arg(long)] animation_bone_map: Option, #[arg(long)] animation_rotation_y: Option, #[arg(long)] animation_scale: Option, #[arg(long)] animation_apply_ibm: bool, /// Spins the object #[arg(long)] debug_spin: bool, /// Adds a light #[arg(long)] debug_light: bool, #[arg(long)] no_particles: bool, #[arg(long)] no_animations: bool, } fn main() -> Result<()> { env_logger::init_from_env("LOG"); let args = Args::parse(); let store = if args.use_cache && !args.pack.is_some() { ResourceStore::new_env().context("opening resource store")? } else { ResourceStore::new_memory() }; let mut prefabs = Vec::new(); let texture_cache = Arc::new(Mutex::new(HashMap::new())); for scenepath in &args.scene { prefabs.push( import_prefab(&store, &texture_cache, scenepath, &args) .context(anyhow!("in {scenepath:?}"))?, ); } let mut size = 0; store.iter(|_k, len| size += len).unwrap(); info!( "prefab has network size of {}", humansize::format_size(size, BINARY) ); if args.dry_run { return Ok(()); } if let Some(outpath) = args.pack { let entry = store.set(&RespackEntry { c_prefab: prefabs })?; let mut resources = Vec::new(); store.iter(|r, _| resources.push(r))?; save_respack( BufWriter::new(File::create(outpath)?), &store, &resources, Some(entry), )?; } else if let Some(address) = args.address { let mut sock = TcpStream::connect(address)?; Packet::Connect(random()).write(&mut sock)?; for p in &prefabs { Packet::AnnouncePrefab(p.clone()).write(&mut sock)?; } sock.flush()?; let mut obs = Vec::new(); if args.add { for (i, p) in prefabs.iter().enumerate() { let ob = Object::new(); info!("adding object {ob}"); Packet::Add(ob, p.clone()).write(&mut sock)?; if args.line_up { Packet::Position(ob, vec3a(i as f32 * 1.2, 0., i as f32 * 0.3), Vec3A::ZERO) .write(&mut sock)?; } obs.push(ob); } sock.flush()?; } if args.debug_spin { let ob = obs[0]; let mut sock2 = sock.try_clone().unwrap(); thread::spawn(move || { let mut x = 0.; loop { Packet::Position(ob, Vec3A::ZERO, vec3a(0., x * 0.1, 0.)) .write(&mut sock2) .unwrap(); sock2.flush().unwrap(); x += 0.1; sleep(Duration::from_millis(50)); } }); } if args.push { if args.use_cache { return Ok(()); } store.iter(|k, _| { Packet::RespondResource(k, Data(store.get_raw(k).unwrap().unwrap())) .write(&mut sock) .unwrap(); })?; sock.flush()?; } else { loop { let packet = Packet::read(&mut sock)?; match packet { Packet::RequestResource(hash) => { if let Some(d) = store.get_raw(hash)? { Packet::RespondResource(hash, Data(d)).write(&mut sock)?; sock.flush()?; } } Packet::Add(ob_a, _) => { if args.clear && !obs.contains(&ob_a) { info!("removing object {ob_a}"); Packet::Remove(ob_a).write(&mut sock)?; sock.flush()?; } } _ => (), } } } } else { bail!("no output option specified. either provide an address to a server or use --pack") } Ok(()) } pub type TextureCache = Arc>>>>; fn load_texture( name: &str, store: &ResourceStore, path: &Path, buffers: &[gltf::buffer::Data], source: &Source, webp: bool, texture_cache: &TextureCache, ) -> Result>> { let (mut image, uri) = match source { gltf::image::Source::View { view, mime_type } => { debug!("{name} texture is embedded and of type {mime_type:?}"); let buf = &buffers[view.buffer().index()].0[view.offset()..view.offset() + view.length()]; (Image(Cow::Borrowed(buf)), None) } gltf::image::Source::Uri { uri, mime_type: Some(mime_type), } => { debug!("{name} texture is {uri:?} and of type {mime_type:?}"); if let Some(res) = texture_cache.lock().unwrap().get(*uri) { return Ok(res.to_owned()); } let path = path.join(uri); let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; (Image(buf.into()), Some(uri.to_string())) } gltf::image::Source::Uri { uri, mime_type: None, } => { debug!("{name} texture is {uri:?} and has no type"); if let Some(res) = texture_cache.lock().unwrap().get(*uri) { return Ok(res.to_owned()); } let path = path.join(uri); let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; (Image(buf.into()), Some(uri.to_string())) } }; if webp { let mut image_out = Vec::new(); let len = image.0.len(); ImageReader::new(Cursor::new(image.0)) .with_guessed_format()? .decode()? .write_with_encoder(WebPEncoder::new_lossless(&mut image_out))?; debug!("webp encode: {len} -> {}", image_out.len()); image = Image(Cow::Owned(image_out)); } let res = Resource(store.set(&image)?.0, PhantomData); if let Some(uri) = uri { texture_cache.lock().unwrap().insert(uri, res.clone()); } Ok(res) } pub fn transform_to_affine(trans: Transform) -> Affine3A { let mat = trans.matrix(); Affine3A::from_cols_array_2d(&[ [mat[0][0], mat[0][1], mat[0][2]], [mat[1][0], mat[1][1], mat[1][2]], [mat[2][0], mat[2][1], mat[2][2]], [mat[3][0], mat[3][1], mat[3][2]], ]) }