/*
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 image::{ImageReader, codecs::webp::WebPEncoder};
use log::info;
use mesh::import_mesh;
use physics::import_physics;
use rand::random;
use std::{
fs::File,
io::{Cursor, Read, Write},
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,
}
fn main() -> Result<()> {
env_logger::init_from_env("LOG");
let args = Args::parse();
let mut sock = TcpStream::connect(args.address)?;
let store = ResourceStore::new_memory();
Packet::Connect(random()).write(&mut sock)?;
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 = Prefab::default();
prefab.name = args.name.clone().or(gltf
.default_scene()
.map(|n| n.name())
.flatten()
.map(|n| n.to_owned()));
for node in gltf.nodes() {
if let Some(mesh) = node.mesh() {
info!("--- MESH ---");
import_mesh(mesh, &buffers, &store, path_base, &node, &mut prefab, &args)?;
}
let (position, _, _) = node.transform().decomposed();
if let Some(light) = node.light() {
info!("--- LIGHT ---");
let emission = Some(Vec3A::from_array(light.color()) * light.intensity());
if let Some(e) = emission {
info!("emission is {e}");
}
prefab.light.push((
Vec3A::from_array(position),
store.set(&LightPart {
emission,
..Default::default()
})?,
));
}
import_physics(&gltf, &node, &mut prefab, &store, &buffers, &args)?;
}
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(buf))?),
..Default::default()
})?);
}
let pres = store.set(&prefab)?;
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 } => {
info!("{name} texture is embedded and of type {mime_type:?}");
let buf =
&buffers[view.buffer().index()].0[view.offset()..view.offset() + view.length()];
Image(buf.to_vec())
}
gltf::image::Source::Uri {
uri,
mime_type: Some(mime_type),
} => {
info!("{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)
}
gltf::image::Source::Uri {
uri,
mime_type: None,
} => {
info!("{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)
}
};
if webp {
let mut image_out = Image(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.0))?;
info!("webp encode: {len} -> {}", image_out.0.len());
image = image_out;
}
Ok(store.set(&image)?)
}