Update Metadata on Mapfix #263

Merged
Quaternions merged 6 commits from metadata-maintenance into staging 2025-08-12 21:56:08 +00:00
10 changed files with 189 additions and 43 deletions

4
Cargo.lock generated
View File

@@ -1520,9 +1520,9 @@ dependencies = [
[[package]]
name = "rbx_asset"
version = "0.4.9"
version = "0.4.10"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "32c7c8efca16fc09ac623ea41cde2bf48e2da761ac8edd575b0b7022f5dc5bd5"
checksum = "a711a8c43b4bbcd3c72832e51a680e407b3a062e1ddc66cb90e57b86c0e65f80"
dependencies = [
"bytes",
"chrono",

View File

@@ -53,8 +53,11 @@ Prerequisite: rust installed
Environment Variables:
- ROBLOX_GROUP_ID
- RBXCOOKIE
- RBX_API_KEY
- API_HOST_INTERNAL
- NATS_HOST
- LOAD_ASSET_VERSION_PLACE_ID
- LOAD_ASSET_VERSION_UNIVERSE_ID
#### License

View File

@@ -6,7 +6,7 @@ edition = "2024"
[dependencies]
async-nats = "0.42.0"
futures = "0.3.31"
rbx_asset = { version = "0.4.9", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" }
rbx_asset = { version = "0.4.10", features = ["gzip", "rustls-tls"], default-features = false, registry = "strafesnet" }
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection_database = "1.0.3"

View File

@@ -214,6 +214,15 @@ impl std::fmt::Display for WormholeElement{
}
}
fn count_sequential(modes:&HashMap<ModeID,Vec<&Instance>>)->usize{
for mode_id in 0..modes.len(){
if !modes.contains_key(&ModeID(mode_id as u64)){
return mode_id;
}
}
return modes.len();
}
/// Count various map elements
#[derive(Default)]
struct Counts<'a>{
@@ -233,6 +242,24 @@ pub struct ModelInfo<'a>{
counts:Counts<'a>,
unanchored_parts:Vec<&'a Instance>,
}
impl ModelInfo<'_>{
pub fn count_modes(&self)->Option<usize>{
let start_zones_count=self.counts.mode_start_counts.len();
let finish_zones_count=self.counts.mode_finish_counts.len();
let sequential_start_zones=count_sequential(&self.counts.mode_start_counts);
let sequential_finish_zones=count_sequential(&self.counts.mode_finish_counts);
// all counts must match
if start_zones_count==finish_zones_count
&& sequential_start_zones==sequential_finish_zones
&& start_zones_count==sequential_start_zones
&& finish_zones_count==sequential_finish_zones
{
Some(start_zones_count)
}else{
None
}
}
}
pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{
// extract model info
@@ -525,14 +552,6 @@ impl<'a> ModelInfo<'a>{
.check(&self.counts.mode_start_counts);
// There must not be non-sequential modes. If Bonus100 exists, Bonuses 1-99 had better also exist.
fn count_sequential(modes:&HashMap<ModeID,Vec<&Instance>>)->usize{
for mode_id in 0..modes.len(){
if !modes.contains_key(&ModeID(mode_id as u64)){
return mode_id;
}
}
return modes.len();
}
let modes_sequential={
let sequential=count_sequential(&self.counts.mode_start_counts);
if sequential==self.counts.mode_start_counts.len(){

View File

@@ -0,0 +1,21 @@
use crate::endpoint;
use rust_grpc::maps_extended::*;
pub type ServiceClient=rust_grpc::maps_extended::maps_service_client::MapsServiceClient<tonic::transport::channel::Channel>;
#[derive(Clone)]
pub struct Client{
client:ServiceClient,
}
impl Client{
pub fn new(
client:ServiceClient,
)->Self{
Self{client}
}
// endpoint!(get,MapId,MapResponse);
// endpoint!(get_list,MapIdList,MapList);
endpoint!(update,MapUpdate,NullResponse);
// endpoint!(create,MapCreate,MapId);
// endpoint!(delete,MapId,NullResponse);
// endpoint!(list,ListRequest,MapList);
// endpoint!(increment_load_count,MapId,NullResponse);
}

View File

@@ -1,6 +1,7 @@
pub mod error;
pub mod maps_extended;
pub mod mapfixes;
pub mod operations;
pub mod scripts;

View File

@@ -47,6 +47,8 @@ async fn main()->Result<(),StartupError>{
},
Err(e)=>panic!("{e}: ROBLOX_GROUP_ID env required"),
};
let load_asset_version_place_id=std::env::var("LOAD_ASSET_VERSION_PLACE_ID").expect("LOAD_ASSET_VERSION_PLACE_ID env required").parse().expect("LOAD_ASSET_VERSION_PLACE_ID int parse failed");
let load_asset_version_universe_id=std::env::var("LOAD_ASSET_VERSION_UNIVERSE_ID").expect("LOAD_ASSET_VERSION_UNIVERSE_ID env required").parse().expect("LOAD_ASSET_VERSION_UNIVERSE_ID int parse failed");
// create / upload models through STRAFESNET_CI2 account
let cookie=std::env::var("RBXCOOKIE").expect("RBXCOOKIE env required");
@@ -59,12 +61,25 @@ async fn main()->Result<(),StartupError>{
let api_host_internal=std::env::var("API_HOST_INTERNAL").expect("API_HOST_INTERNAL env required");
let endpoint=tonic::transport::Endpoint::new(api_host_internal).map_err(StartupError::API)?;
let channel=endpoint.connect_lazy();
let mapfixes=crate::grpc::mapfixes::ValidatorMapfixesServiceClient::new(channel.clone());
let operations=crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone());
let scripts=crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone());
let script_policy=crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone());
let submissions=crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel);
let message_handler=message_handler::MessageHandler::new(cloud_context,cookie_context,group_id,mapfixes,operations,scripts,script_policy,submissions);
let mapfixes=crate::grpc::mapfixes::Service::new(crate::grpc::mapfixes::ValidatorMapfixesServiceClient::new(channel.clone()));
let operations=crate::grpc::operations::Service::new(crate::grpc::operations::ValidatorOperationsServiceClient::new(channel.clone()));
let scripts=crate::grpc::scripts::Service::new(crate::grpc::scripts::ValidatorScriptsServiceClient::new(channel.clone()));
let script_policy=crate::grpc::script_policy::Service::new(crate::grpc::script_policy::ValidatorScriptPolicyServiceClient::new(channel.clone()));
let submissions=crate::grpc::submissions::Service::new(crate::grpc::submissions::ValidatorSubmissionsServiceClient::new(channel.clone()));
let maps_extended=crate::grpc::maps_extended::Client::new(crate::grpc::maps_extended::ServiceClient::new(channel));
let message_handler=message_handler::MessageHandler{
cloud_context,
cookie_context,
group_id,
load_asset_version_place_id,
load_asset_version_universe_id,
maps_extended,
mapfixes,
operations,
scripts,
script_policy,
submissions,
};
// nats
let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required");

View File

@@ -31,6 +31,9 @@ pub struct MessageHandler{
pub(crate) cloud_context:rbx_asset::cloud::Context,
pub(crate) cookie_context:rbx_asset::cookie::Context,
pub(crate) group_id:Option<u64>,
pub(crate) load_asset_version_place_id:u64,
pub(crate) load_asset_version_universe_id:u64,
pub(crate) maps_extended:crate::grpc::maps_extended::Client,
pub(crate) mapfixes:crate::grpc::mapfixes::Service,
pub(crate) operations:crate::grpc::operations::Service,
pub(crate) scripts:crate::grpc::scripts::Service,
@@ -39,27 +42,6 @@ pub struct MessageHandler{
}
impl MessageHandler{
pub fn new(
cloud_context:rbx_asset::cloud::Context,
cookie_context:rbx_asset::cookie::Context,
group_id:Option<u64>,
mapfixes:crate::grpc::mapfixes::ValidatorMapfixesServiceClient,
operations:crate::grpc::operations::ValidatorOperationsServiceClient,
scripts:crate::grpc::scripts::ValidatorScriptsServiceClient,
script_policy:crate::grpc::script_policy::ValidatorScriptPolicyServiceClient,
submissions:crate::grpc::submissions::ValidatorSubmissionsServiceClient,
)->Self{
Self{
cloud_context,
cookie_context,
group_id,
mapfixes:crate::grpc::mapfixes::Service::new(mapfixes),
operations:crate::grpc::operations::Service::new(operations),
scripts:crate::grpc::scripts::Service::new(scripts),
script_policy:crate::grpc::script_policy::Service::new(script_policy),
submissions:crate::grpc::submissions::Service::new(submissions),
}
}
pub async fn handle_message_result(&self,message_result:MessageResult)->Result<(),HandleMessageError>{
let message=message_result.map_err(HandleMessageError::Messages)?;
message.double_ack().await.map_err(HandleMessageError::DoubleAck)?;

View File

@@ -1,4 +1,3 @@
#[allow(dead_code)]
#[derive(Debug)]
pub enum ReadDomError{
@@ -112,3 +111,21 @@ pub fn get_mapinfo<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&rbx_dom_wea
game_id:model_instance.name.parse(),
}
}
pub async fn get_luau_result_exp_backoff(
context:&rbx_asset::cloud::Context,
luau_session:&rbx_asset::cloud::LuauSessionResponse
)->Result<Result<rbx_asset::cloud::LuauResults,rbx_asset::cloud::LuauError>,rbx_asset::cloud::LuauSessionError>{
const BACKOFF_MUL:f32=1.395_612_5;//exp(1/3)
let mut backoff=1000f32;
loop{
match luau_session.try_get_result(context).await{
//try again when the operation is not done
Err(rbx_asset::cloud::LuauSessionError::NotDone)=>(),
//return all other results
other_result=>return other_result,
}
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
backoff*=BACKOFF_MUL;
}
}

View File

@@ -9,6 +9,21 @@ pub enum Error{
Json(serde_json::Error),
Upload(rbx_asset::cookie::UploadError),
ApiActionMapfixUploaded(tonic::Status),
ModelFileDecode(crate::rbx_util::ReadDomError),
GetRootInstance(crate::rbx_util::GetRootInstanceError),
NonSequentialModes,
TooManyModes(usize),
CreateSession(rbx_asset::cloud::CreateError),
NonPositiveNumber(serde_json::Number),
Script(rbx_asset::cloud::LuauError),
InvalidResult(Vec<serde_json::Value>),
LuauSession(rbx_asset::cloud::LuauSessionError),
GetAssetInfo(rbx_asset::cloud::GetError),
RevisionMismatch{
after:u64,
before:u64,
},
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@@ -29,22 +44,95 @@ impl crate::message_handler::MessageHandler{
let model_data=maybe_gzip.to_vec().map_err(Error::IO)?;
// upload the map to the strafesnet group
let _upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{
let upload_response=self.cookie_context.upload(rbx_asset::cookie::UploadRequest{
assetid:upload_info.TargetAssetID,
groupId:self.group_id,
name:None,
description:None,
ispublic:None,
allowComments:None,
},model_data).await.map_err(Error::Upload)?;
// that's it, the database entry does not need to be changed.
},model_data.clone()).await.map_err(Error::Upload)?;
// mark mapfix as uploaded, TargetAssetID is unchanged
self.mapfixes.set_status_uploaded(rust_grpc::validator::MapfixId{
id:upload_info.MapfixID,
}).await.map_err(Error::ApiActionMapfixUploaded)?;
// count modes
// decode dom (slow!)
let dom=crate::rbx_util::read_dom(model_data.as_slice()).map_err(Error::ModelFileDecode)?;
// extract the root instance
let model_instance=crate::rbx_util::get_root_instance(&dom).map_err(Error::GetRootInstance)?;
// extract information from the model
let model_info=crate::check::get_model_info(&dom,model_instance);
// count modes
let modes=model_info.count_modes().ok_or(Error::NonSequentialModes)?;
// hard limit LOL
let modes=if modes<u32::MAX as usize{
modes as u32
}else{
return Err(Error::TooManyModes(modes));
};
// update asset version and modes using Roblox Luau API
let script=format!("return game:GetService(\"InsertService\"):GetLatestAssetVersionAsync({})",upload_info.TargetAssetID);
let request=rbx_asset::cloud::LuauSessionLatestRequest{
place_id:self.load_asset_version_place_id,
universe_id:self.load_asset_version_universe_id,
};
let session=rbx_asset::cloud::LuauSessionCreate{
script:&script,
user:None,
timeout:None,
binaryInput:None,
enableBinaryOutput:None,
binaryOutputUri:None,
};
let session_response=self.cloud_context.create_luau_session(&request,session).await.map_err(Error::CreateSession)?;
let result=crate::rbx_util::get_luau_result_exp_backoff(&self.cloud_context,&session_response).await;
let load_asset_version=match result{
Ok(Ok(rbx_asset::cloud::LuauResults{results}))=>match results.as_slice(){
[serde_json::Value::Number(load_asset_version)]=>load_asset_version.as_u64().ok_or_else(||Error::NonPositiveNumber(load_asset_version.clone())),
_=>Err(Error::InvalidResult(results))
},
Ok(Err(e))=>Err(Error::Script(e)),
Err(e)=>Err(Error::LuauSession(e)),
}?;
// check asset version to make sure it hasn't been updated
let asset_response=self.cloud_context.get_asset_info(rbx_asset::cloud::GetAssetLatestRequest{
asset_id:upload_info.TargetAssetID,
}).await.map_err(Error::GetAssetInfo)?;
if upload_response.AssetVersion!=asset_response.revisionId{
// the model was updated while we were fetching LoadAssetVersion.
// the number we got may be invalid.
return Err(Error::RevisionMismatch{
before:upload_response.AssetVersion,
after:asset_response.revisionId,
});
}
// write AssetVersion directly to map
self.maps_extended.update(rust_grpc::maps_extended::MapUpdate{
id:upload_info.TargetAssetID as i64,
asset_version:Some(load_asset_version),
modes:Some(modes),
display_name:None,
creator:None,
game_id:None,
date:None,
submitter:None,
thumbnail:None,
}).await.map_err(Error::ApiActionMapfixUploaded)?;
Ok(())
}
}