/* 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)] pub mod mesh; pub mod physics; use anyhow::Result; use clap::Parser; use gltf::{Gltf, image::Source, import_buffers}; use humansize::BINARY; use image::{ImageReader, codecs::webp::WebPEncoder}; use log::{debug, info}; use mesh::import_mesh; use physics::import_physics; use rand::random; use rayon::iter::{ParallelBridge, ParallelIterator}; use std::{ borrow::Cow, fs::File, io::{Cursor, Read, Write}, marker::PhantomData, net::{SocketAddr, TcpStream}, path::{Path, PathBuf}, thread::{self, sleep}, time::Duration, }; use weareshared::{ Vec3A, helper::ReadWrite, packets::{Data, Object, Packet, Resource}, resources::{EnvironmentPart, Image, LightPart, Prefab}, store::ResourceStore, vec3a, }; #[derive(Parser)] pub struct Args { address: SocketAddr, /// Path to a glTF file, binary or json format scene: PathBuf, /// Send all resources to the server then quit #[arg(short, long)] push: bool, /// Spin the object #[arg(long)] spin: 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)] z_up: bool, #[arg(short, long)] dry_run: bool, } fn main() -> Result<()> { env_logger::init_from_env("LOG"); let args = Args::parse(); let store = ResourceStore::new_memory(); let path_base = args.scene.parent().unwrap(); let mut gltf = Gltf::from_reader_without_validation(File::open(&args.scene)?)?; let blob = gltf.blob.take(); let buffers = import_buffers(&gltf, Some(path_base), blob)?; let mut prefab = gltf .nodes() .par_bridge() .map(|node| { let mut prefab = Prefab::default(); if let Some(mesh) = node.mesh() { import_mesh(mesh, &buffers, &store, path_base, &node, &mut prefab, &args)?; } let (position, _, _) = node.transform().decomposed(); if let Some(light) = node.light() { let name = node.name().map(|e| e.to_owned()); if let Some(name) = &name { info!("adding light {name:?}"); } else { info!("adding light"); } let emission = Some(Vec3A::from_array(light.color()) * light.intensity()); if let Some(e) = emission { debug!("emission is {e}"); } prefab.light.push(( Vec3A::from_array(position), store.set(&LightPart { emission, name, radius: None, })?, )); } import_physics(&gltf, &node, &mut prefab, &store, &buffers, &args)?; Ok::<_, anyhow::Error>(prefab) }) .reduce( || Ok(Prefab::default()), |a, b| { let mut a = a?; let b = b?; a.collision.extend(b.collision); a.mesh.extend(b.mesh); a.light.extend(b.light); a.environment = a.environment.or(b.environment); a.name = a.name.or(b.name); Ok(a) }, )?; prefab.name = args.name.clone().or(gltf .default_scene() .map(|n| n.name()) .flatten() .map(|n| n.to_owned())); if let Some(skybox) = args.skybox { let mut buf = Vec::new(); File::open(skybox)?.read_to_end(&mut buf)?; prefab.environment = Some(store.set(&EnvironmentPart { skybox: Some(store.set(&Image(Cow::Owned(buf)))?), ..Default::default() })?); } let pres = store.set(&prefab)?; let mut size = 0; store.iter(|d| size += d.len()).unwrap(); info!( "prefab has network size of {}", humansize::format_size(size, BINARY) ); if args.dry_run { return Ok(()); } let mut sock = TcpStream::connect(args.address)?; Packet::Connect(random()).write(&mut sock)?; Packet::AnnouncePrefab(pres.clone()).write(&mut sock)?; sock.flush()?; let ob = if args.add { let ob = Object::new(); Packet::Add(ob, pres.clone()).write(&mut sock)?; sock.flush()?; Some(ob) } else { None }; if args.spin { let ob = ob.clone().unwrap(); let mut sock2 = sock.try_clone().unwrap(); thread::spawn(move || { let mut x = 0.; loop { Packet::Position(ob, Vec3A::ZERO, vec3a(x, x * 0.3, x * 0.1)) .write(&mut sock2) .unwrap(); sock2.flush().unwrap(); x += 0.1; sleep(Duration::from_millis(50)); } }); } if args.push { store.iter(|d| { Packet::RespondResource(Data(d.to_vec())) .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(Data(d)).write(&mut sock)?; sock.flush()?; } } Packet::Add(ob_a, _) => { if args.clear { if Some(ob_a) != ob { info!("removing object {ob_a}"); Packet::Remove(ob_a).write(&mut sock)?; sock.flush()?; } } } _ => (), } } } Ok(()) } fn load_texture( name: &str, store: &ResourceStore, path: &Path, buffers: &[gltf::buffer::Data], source: &Source, webp: bool, ) -> Result>> { let mut image = 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)) } gltf::image::Source::Uri { uri, mime_type: Some(mime_type), } => { debug!("{name} texture is {uri:?} and of type {mime_type:?}"); let path = path.join(uri); let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; Image(buf.into()) } gltf::image::Source::Uri { uri, mime_type: None, } => { debug!("{name} texture is {uri:?} and has no type"); let path = path.join(uri); let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; Image(buf.into()) } }; 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)); } Ok(Resource(store.set(&image)?.0, PhantomData)) }