Update Metadata on Mapfix #263
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(){
|
||||
|
||||
21
validation/src/grpc/maps_extended.rs
Normal file
21
validation/src/grpc/maps_extended.rs
Normal 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);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub mod maps_extended;
|
||||
pub mod mapfixes;
|
||||
pub mod operations;
|
||||
pub mod scripts;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user