/*
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]],
])
}