Add Submission Fields + Rustify Map Check #126
13
openapi.yaml
13
openapi.yaml
@ -1641,12 +1641,25 @@ components:
|
|||||||
SubmissionTriggerCreate:
|
SubmissionTriggerCreate:
|
||||||
required:
|
required:
|
||||||
- AssetID
|
- AssetID
|
||||||
|
- DisplayName
|
||||||
|
- Creator
|
||||||
|
- GameID
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
AssetID:
|
AssetID:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
minimum: 0
|
minimum: 0
|
||||||
|
DisplayName:
|
||||||
|
type: string
|
||||||
|
maxLength: 128
|
||||||
|
Creator:
|
||||||
|
type: string
|
||||||
|
maxLength: 128
|
||||||
|
GameID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
minimum: 0
|
||||||
ReleaseInfo:
|
ReleaseInfo:
|
||||||
required:
|
required:
|
||||||
- SubmissionID
|
- SubmissionID
|
||||||
|
@ -3128,10 +3128,25 @@ func (s *SubmissionTriggerCreate) encodeFields(e *jx.Encoder) {
|
|||||||
e.FieldStart("AssetID")
|
e.FieldStart("AssetID")
|
||||||
e.Int64(s.AssetID)
|
e.Int64(s.AssetID)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
e.FieldStart("DisplayName")
|
||||||
|
e.Str(s.DisplayName)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
e.FieldStart("Creator")
|
||||||
|
e.Str(s.Creator)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
e.FieldStart("GameID")
|
||||||
|
e.Int32(s.GameID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonFieldsNameOfSubmissionTriggerCreate = [1]string{
|
var jsonFieldsNameOfSubmissionTriggerCreate = [4]string{
|
||||||
0: "AssetID",
|
0: "AssetID",
|
||||||
|
1: "DisplayName",
|
||||||
|
2: "Creator",
|
||||||
|
3: "GameID",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode decodes SubmissionTriggerCreate from json.
|
// Decode decodes SubmissionTriggerCreate from json.
|
||||||
@ -3155,6 +3170,42 @@ func (s *SubmissionTriggerCreate) Decode(d *jx.Decoder) error {
|
|||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return errors.Wrap(err, "decode field \"AssetID\"")
|
return errors.Wrap(err, "decode field \"AssetID\"")
|
||||||
}
|
}
|
||||||
|
case "DisplayName":
|
||||||
|
requiredBitSet[0] |= 1 << 1
|
||||||
|
if err := func() error {
|
||||||
|
v, err := d.Str()
|
||||||
|
s.DisplayName = string(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return errors.Wrap(err, "decode field \"DisplayName\"")
|
||||||
|
}
|
||||||
|
case "Creator":
|
||||||
|
requiredBitSet[0] |= 1 << 2
|
||||||
|
if err := func() error {
|
||||||
|
v, err := d.Str()
|
||||||
|
s.Creator = string(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return errors.Wrap(err, "decode field \"Creator\"")
|
||||||
|
}
|
||||||
|
case "GameID":
|
||||||
|
requiredBitSet[0] |= 1 << 3
|
||||||
|
if err := func() error {
|
||||||
|
v, err := d.Int32()
|
||||||
|
s.GameID = int32(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return errors.Wrap(err, "decode field \"GameID\"")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return d.Skip()
|
return d.Skip()
|
||||||
}
|
}
|
||||||
@ -3165,7 +3216,7 @@ func (s *SubmissionTriggerCreate) Decode(d *jx.Decoder) error {
|
|||||||
// Validate required fields.
|
// Validate required fields.
|
||||||
var failures []validate.FieldError
|
var failures []validate.FieldError
|
||||||
for i, mask := range [1]uint8{
|
for i, mask := range [1]uint8{
|
||||||
0b00000001,
|
0b00001111,
|
||||||
} {
|
} {
|
||||||
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
|
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
|
||||||
// Mask only required fields and check equality to mask using XOR.
|
// Mask only required fields and check equality to mask using XOR.
|
||||||
|
@ -1318,7 +1318,10 @@ func (s *Submission) SetStatusID(val int32) {
|
|||||||
|
|
||||||
// Ref: #/components/schemas/SubmissionTriggerCreate
|
// Ref: #/components/schemas/SubmissionTriggerCreate
|
||||||
type SubmissionTriggerCreate struct {
|
type SubmissionTriggerCreate struct {
|
||||||
AssetID int64 `json:"AssetID"`
|
AssetID int64 `json:"AssetID"`
|
||||||
|
DisplayName string `json:"DisplayName"`
|
||||||
|
Creator string `json:"Creator"`
|
||||||
|
GameID int32 `json:"GameID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetID returns the value of AssetID.
|
// GetAssetID returns the value of AssetID.
|
||||||
@ -1326,11 +1329,41 @@ func (s *SubmissionTriggerCreate) GetAssetID() int64 {
|
|||||||
return s.AssetID
|
return s.AssetID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDisplayName returns the value of DisplayName.
|
||||||
|
func (s *SubmissionTriggerCreate) GetDisplayName() string {
|
||||||
|
return s.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCreator returns the value of Creator.
|
||||||
|
func (s *SubmissionTriggerCreate) GetCreator() string {
|
||||||
|
return s.Creator
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGameID returns the value of GameID.
|
||||||
|
func (s *SubmissionTriggerCreate) GetGameID() int32 {
|
||||||
|
return s.GameID
|
||||||
|
}
|
||||||
|
|
||||||
// SetAssetID sets the value of AssetID.
|
// SetAssetID sets the value of AssetID.
|
||||||
func (s *SubmissionTriggerCreate) SetAssetID(val int64) {
|
func (s *SubmissionTriggerCreate) SetAssetID(val int64) {
|
||||||
s.AssetID = val
|
s.AssetID = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDisplayName sets the value of DisplayName.
|
||||||
|
func (s *SubmissionTriggerCreate) SetDisplayName(val string) {
|
||||||
|
s.DisplayName = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCreator sets the value of Creator.
|
||||||
|
func (s *SubmissionTriggerCreate) SetCreator(val string) {
|
||||||
|
s.Creator = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGameID sets the value of GameID.
|
||||||
|
func (s *SubmissionTriggerCreate) SetGameID(val int32) {
|
||||||
|
s.GameID = val
|
||||||
|
}
|
||||||
|
|
||||||
// Ref: #/components/schemas/Submissions
|
// Ref: #/components/schemas/Submissions
|
||||||
type Submissions struct {
|
type Submissions struct {
|
||||||
Total int64 `json:"Total"`
|
Total int64 `json:"Total"`
|
||||||
|
@ -1802,6 +1802,64 @@ func (s *SubmissionTriggerCreate) Validate() error {
|
|||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := (validate.String{
|
||||||
|
MinLength: 0,
|
||||||
|
MinLengthSet: false,
|
||||||
|
MaxLength: 128,
|
||||||
|
MaxLengthSet: true,
|
||||||
|
Email: false,
|
||||||
|
Hostname: false,
|
||||||
|
Regex: nil,
|
||||||
|
}).Validate(string(s.DisplayName)); err != nil {
|
||||||
|
return errors.Wrap(err, "string")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
failures = append(failures, validate.FieldError{
|
||||||
|
Name: "DisplayName",
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := (validate.String{
|
||||||
|
MinLength: 0,
|
||||||
|
MinLengthSet: false,
|
||||||
|
MaxLength: 128,
|
||||||
|
MaxLengthSet: true,
|
||||||
|
Email: false,
|
||||||
|
Hostname: false,
|
||||||
|
Regex: nil,
|
||||||
|
}).Validate(string(s.Creator)); err != nil {
|
||||||
|
return errors.Wrap(err, "string")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
failures = append(failures, validate.FieldError{
|
||||||
|
Name: "Creator",
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := func() error {
|
||||||
|
if err := (validate.Int{
|
||||||
|
MinSet: true,
|
||||||
|
Min: 0,
|
||||||
|
MaxSet: false,
|
||||||
|
Max: 0,
|
||||||
|
MinExclusive: false,
|
||||||
|
MaxExclusive: false,
|
||||||
|
MultipleOfSet: false,
|
||||||
|
MultipleOf: 0,
|
||||||
|
}).Validate(int64(s.GameID)); err != nil {
|
||||||
|
return errors.Wrap(err, "int")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
failures = append(failures, validate.FieldError{
|
||||||
|
Name: "GameID",
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
return &validate.Error{Fields: failures}
|
return &validate.Error{Fields: failures}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,9 @@ type CreateSubmissionRequest struct {
|
|||||||
// operation_id is passed back in the response message
|
// operation_id is passed back in the response message
|
||||||
OperationID int32
|
OperationID int32
|
||||||
ModelID uint64
|
ModelID uint64
|
||||||
|
DisplayName string
|
||||||
|
Creator string
|
||||||
|
GameID uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateMapfixRequest struct {
|
type CreateMapfixRequest struct {
|
||||||
|
@ -101,8 +101,11 @@ func (svc *Service) CreateSubmission(ctx context.Context, request *api.Submissio
|
|||||||
}
|
}
|
||||||
|
|
||||||
create_request := model.CreateSubmissionRequest{
|
create_request := model.CreateSubmissionRequest{
|
||||||
OperationID: operation.ID,
|
OperationID: operation.ID,
|
||||||
ModelID: ModelID,
|
ModelID: ModelID,
|
||||||
|
DisplayName: request.DisplayName,
|
||||||
|
Creator: request.Creator,
|
||||||
|
GameID: uint32(request.GameID),
|
||||||
}
|
}
|
||||||
|
|
||||||
j, err := json.Marshal(create_request)
|
j, err := json.Marshal(create_request)
|
||||||
|
@ -167,6 +167,9 @@ impl Context{
|
|||||||
);
|
);
|
||||||
action!("submissions",action_submission_submitted,config,ActionSubmissionSubmittedRequest,"status/validator-submitted",config.SubmissionID,
|
action!("submissions",action_submission_submitted,config,ActionSubmissionSubmittedRequest,"status/validator-submitted",config.SubmissionID,
|
||||||
("ModelVersion",config.ModelVersion.to_string().as_str())
|
("ModelVersion",config.ModelVersion.to_string().as_str())
|
||||||
|
("DisplayName",config.DisplayName.as_str())
|
||||||
|
("Creator",config.Creator.as_str())
|
||||||
|
("GameID",config.GameID.to_string().as_str())
|
||||||
);
|
);
|
||||||
action!("submissions",action_submission_validated,config,SubmissionID,"status/validator-validated",config.0,);
|
action!("submissions",action_submission_validated,config,SubmissionID,"status/validator-validated",config.0,);
|
||||||
action!("submissions",update_submission_validated_model,config,UpdateSubmissionModelRequest,"validated-model",config.SubmissionID,
|
action!("submissions",update_submission_validated_model,config,UpdateSubmissionModelRequest,"validated-model",config.SubmissionID,
|
||||||
@ -196,6 +199,9 @@ impl Context{
|
|||||||
);
|
);
|
||||||
action!("mapfixes",action_mapfix_submitted,config,ActionMapfixSubmittedRequest,"status/validator-submitted",config.MapfixID,
|
action!("mapfixes",action_mapfix_submitted,config,ActionMapfixSubmittedRequest,"status/validator-submitted",config.MapfixID,
|
||||||
("ModelVersion",config.ModelVersion.to_string().as_str())
|
("ModelVersion",config.ModelVersion.to_string().as_str())
|
||||||
|
("DisplayName",config.DisplayName.as_str())
|
||||||
|
("Creator",config.Creator.as_str())
|
||||||
|
("GameID",config.GameID.to_string().as_str())
|
||||||
);
|
);
|
||||||
action!("mapfixes",action_mapfix_validated,config,MapfixID,"status/validator-validated",config.0,);
|
action!("mapfixes",action_mapfix_validated,config,MapfixID,"status/validator-validated",config.0,);
|
||||||
action!("mapfixes",update_mapfix_validated_model,config,UpdateMapfixModelRequest,"validated-model",config.MapfixID,
|
action!("mapfixes",update_mapfix_validated_model,config,UpdateMapfixModelRequest,"validated-model",config.MapfixID,
|
||||||
|
@ -228,6 +228,9 @@ pub struct UpdateSubmissionModelRequest{
|
|||||||
pub struct ActionSubmissionSubmittedRequest{
|
pub struct ActionSubmissionSubmittedRequest{
|
||||||
pub SubmissionID:i64,
|
pub SubmissionID:i64,
|
||||||
pub ModelVersion:u64,
|
pub ModelVersion:u64,
|
||||||
|
pub DisplayName:String,
|
||||||
|
pub Creator:String,
|
||||||
|
pub GameID:u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[allow(nonstandard_style)]
|
||||||
@ -267,6 +270,9 @@ pub struct UpdateMapfixModelRequest{
|
|||||||
pub struct ActionMapfixSubmittedRequest{
|
pub struct ActionMapfixSubmittedRequest{
|
||||||
pub MapfixID:i64,
|
pub MapfixID:i64,
|
||||||
pub ModelVersion:u64,
|
pub ModelVersion:u64,
|
||||||
|
pub DisplayName:String,
|
||||||
|
pub Creator:String,
|
||||||
|
pub GameID:u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[allow(nonstandard_style)]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
use std::collections::{HashSet,HashMap};
|
||||||
use crate::download::download_asset_version;
|
use crate::download::download_asset_version;
|
||||||
use crate::rbx_util::{class_is_a,get_mapinfo,get_root_instance,read_dom,MapInfo,ReadDomError};
|
use crate::rbx_util::{class_is_a,get_mapinfo,get_root_instance,read_dom,ReadDomError,GameID,ParseGameIDError,MapInfo,GetRootInstanceError,StringValueError};
|
||||||
|
|
||||||
use heck::{ToSnakeCase,ToTitleCase};
|
use heck::{ToSnakeCase,ToTitleCase};
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ pub enum Error{
|
|||||||
CreatorTypeMustBeUser,
|
CreatorTypeMustBeUser,
|
||||||
Download(crate::download::Error),
|
Download(crate::download::Error),
|
||||||
ModelFileDecode(ReadDomError),
|
ModelFileDecode(ReadDomError),
|
||||||
|
GetRootInstance(GetRootInstanceError),
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for Error{
|
impl std::fmt::Display for Error{
|
||||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
@ -38,117 +40,30 @@ impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum Check{
|
|
||||||
Pass,
|
|
||||||
#[default]
|
|
||||||
Fail,
|
|
||||||
}
|
|
||||||
impl Check{
|
|
||||||
fn pass(&self)->bool{
|
|
||||||
match self{
|
|
||||||
Check::Pass=>true,
|
|
||||||
Check::Fail=>false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for Check{
|
|
||||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
|
||||||
match self{
|
|
||||||
Check::Pass=>write!(f,"passed"),
|
|
||||||
Check::Fail=>write!(f,"failed"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct CheckReport{
|
|
||||||
// === METADATA CHECKS ===
|
|
||||||
// the model must have exactly 1 root part (models uploaded to roblox can have multiple roots)
|
|
||||||
exactly_one_root:Check,
|
|
||||||
// the root must be of class Model
|
|
||||||
root_is_model:Check,
|
|
||||||
// the prefix of the model's name must match the game it was submitted for. bhop_ for bhop, and surf_ for surf
|
|
||||||
model_name_prefix_is_valid:Check,
|
|
||||||
// your model's name must match this regex: ^[a-z0-9_]
|
|
||||||
model_name_is_snake_case:Check,
|
|
||||||
// map must have a StringValue named Creator and DisplayName. additionally, they must both have a value
|
|
||||||
has_display_name:Check,
|
|
||||||
has_creator:Check,
|
|
||||||
// the display name must be capitalized
|
|
||||||
display_name_is_title_case:Check,
|
|
||||||
// you cannot have any Scripts or ModuleScripts that have the keyword 'getfenv" or 'require'
|
|
||||||
// you cannot have more than 50 duplicate scripts
|
|
||||||
|
|
||||||
// === MODE CHECKS ===
|
|
||||||
// Exactly one MapStart
|
|
||||||
exactly_one_mapstart:Check,
|
|
||||||
// At least one MapFinish
|
|
||||||
at_least_one_mapfinish:Check,
|
|
||||||
// Spawn0 or Spawn1 must exist
|
|
||||||
spawn1_exists:Check,
|
|
||||||
// No duplicate Spawn#
|
|
||||||
no_duplicate_spawns:Check,
|
|
||||||
// No duplicate WormholeOut# (duplicate WormholeIn# ok)
|
|
||||||
no_duplicate_wormhole_out:Check,
|
|
||||||
}
|
|
||||||
impl CheckReport{
|
|
||||||
pub fn pass(&self)->bool{
|
|
||||||
return self.exactly_one_root.pass()
|
|
||||||
&&self.root_is_model.pass()
|
|
||||||
&&self.model_name_prefix_is_valid.pass()
|
|
||||||
&&self.model_name_is_snake_case.pass()
|
|
||||||
&&self.has_display_name.pass()
|
|
||||||
&&self.has_creator.pass()
|
|
||||||
&&self.display_name_is_title_case.pass()
|
|
||||||
&&self.exactly_one_mapstart.pass()
|
|
||||||
&&self.at_least_one_mapfinish.pass()
|
|
||||||
&&self.spawn1_exists.pass()
|
|
||||||
&&self.no_duplicate_spawns.pass()
|
|
||||||
&&self.no_duplicate_wormhole_out.pass()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for CheckReport{
|
|
||||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
|
||||||
write!(f,
|
|
||||||
"exactly_one_root={}\n
|
|
||||||
root_is_model={}\n
|
|
||||||
model_name_prefix_is_valid={}\n
|
|
||||||
model_name_is_snake_case={}\n
|
|
||||||
has_display_name={}\n
|
|
||||||
has_creator={}\n
|
|
||||||
display_name_is_title_case={}\n
|
|
||||||
exactly_one_mapstart={}\n
|
|
||||||
at_least_one_mapfinish={}\n
|
|
||||||
spawn1_exists={}\n
|
|
||||||
no_duplicate_spawns={}\n
|
|
||||||
no_duplicate_wormhole_out={}",
|
|
||||||
self.exactly_one_root,
|
|
||||||
self.root_is_model,
|
|
||||||
self.model_name_prefix_is_valid,
|
|
||||||
self.model_name_is_snake_case,
|
|
||||||
self.has_display_name,
|
|
||||||
self.has_creator,
|
|
||||||
self.display_name_is_title_case,
|
|
||||||
self.exactly_one_mapstart,
|
|
||||||
self.at_least_one_mapfinish,
|
|
||||||
self.spawn1_exists,
|
|
||||||
self.no_duplicate_spawns,
|
|
||||||
self.no_duplicate_wormhole_out,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Zone{
|
enum Zone{
|
||||||
Start(ModeID),
|
Start(ModeID),
|
||||||
Finish(ModeID),
|
Finish(ModeID),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug,Hash,Eq,PartialEq)]
|
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||||
struct ModeID(u64);
|
struct ModeID(u64);
|
||||||
impl ModeID{
|
impl ModeID{
|
||||||
const MAIN:Self=Self(0);
|
const MAIN:Self=Self(0);
|
||||||
const BONUS:Self=Self(1);
|
const BONUS:Self=Self(1);
|
||||||
|
fn write_start_zone(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
match self{
|
||||||
|
ModeID(0)=>write!(f,"MapStart"),
|
||||||
|
ModeID(1)=>write!(f,"BonusStart"),
|
||||||
|
ModeID(other)=>write!(f,"Bonus{other}Start"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn write_finish_zone(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
match self{
|
||||||
|
ModeID(0)=>write!(f,"MapFinish"),
|
||||||
|
ModeID(1)=>write!(f,"BonusFinish"),
|
||||||
|
ModeID(other)=>write!(f,"Bonus{other}Finish"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum ZoneParseError{
|
pub enum ZoneParseError{
|
||||||
@ -188,66 +103,37 @@ impl SpawnID{
|
|||||||
struct WormholeOutID(u64);
|
struct WormholeOutID(u64);
|
||||||
|
|
||||||
struct Counts{
|
struct Counts{
|
||||||
mode_start_counts:std::collections::HashMap<ModeID,u64>,
|
mode_start_counts:HashMap<ModeID,u64>,
|
||||||
mode_finish_counts:std::collections::HashMap<ModeID,u64>,
|
mode_finish_counts:HashMap<ModeID,u64>,
|
||||||
spawn_counts:std::collections::HashMap<SpawnID,u64>,
|
spawn_counts:HashMap<SpawnID,u64>,
|
||||||
wormhole_out_counts:std::collections::HashMap<WormholeOutID,u64>,
|
wormhole_out_counts:HashMap<WormholeOutID,u64>,
|
||||||
}
|
}
|
||||||
impl Counts{
|
impl Counts{
|
||||||
fn new()->Self{
|
fn new()->Self{
|
||||||
Self{
|
Self{
|
||||||
mode_start_counts:std::collections::HashMap::new(),
|
mode_start_counts:HashMap::new(),
|
||||||
mode_finish_counts:std::collections::HashMap::new(),
|
mode_finish_counts:HashMap::new(),
|
||||||
spawn_counts:std::collections::HashMap::new(),
|
spawn_counts:HashMap::new(),
|
||||||
wormhole_out_counts:std::collections::HashMap::new(),
|
wormhole_out_counts:HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(dom:&rbx_dom_weak::WeakDom)->CheckReport{
|
|
||||||
// empty report with all checks failed
|
|
||||||
let mut report=CheckReport::default();
|
|
||||||
|
|
||||||
|
pub struct ModelInfo<'a>{
|
||||||
|
model_class:&'a str,
|
||||||
|
model_name:&'a str,
|
||||||
|
map_info:MapInfo<'a>,
|
||||||
|
counts:Counts,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_model_info(dom:&rbx_dom_weak::WeakDom)->Result<ModelInfo,GetRootInstanceError>{
|
||||||
// extract the root instance, otherwise immediately return
|
// extract the root instance, otherwise immediately return
|
||||||
let Ok(model_instance)=get_root_instance(&dom)else{
|
let model_instance=get_root_instance(&dom)?;
|
||||||
return report;
|
|
||||||
};
|
|
||||||
|
|
||||||
report.exactly_one_root=Check::Pass;
|
|
||||||
|
|
||||||
if model_instance.class=="Model"{
|
|
||||||
report.root_is_model=Check::Pass;
|
|
||||||
}
|
|
||||||
if model_instance.name==model_instance.name.to_snake_case(){
|
|
||||||
report.model_name_is_snake_case=Check::Pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract model info
|
// extract model info
|
||||||
let MapInfo{display_name,creator,game_id}=get_mapinfo(&dom,model_instance);
|
let map_info=get_mapinfo(&dom,model_instance);
|
||||||
|
|
||||||
// check DisplayName
|
|
||||||
if let Ok(display_name)=display_name{
|
|
||||||
if !display_name.is_empty(){
|
|
||||||
report.has_display_name=Check::Pass;
|
|
||||||
if display_name==display_name.to_title_case(){
|
|
||||||
report.display_name_is_title_case=Check::Pass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check Creator
|
|
||||||
if let Ok(creator)=creator{
|
|
||||||
if !creator.is_empty(){
|
|
||||||
report.has_creator=Check::Pass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check GameID
|
|
||||||
if game_id.is_ok(){
|
|
||||||
report.model_name_prefix_is_valid=Check::Pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === MODE CHECKS ===
|
|
||||||
// count objects
|
// count objects
|
||||||
let mut counts=Counts::new();
|
let mut counts=Counts::new();
|
||||||
for instance in dom.descendants_of(model_instance.referent()){
|
for instance in dom.descendants_of(model_instance.referent()){
|
||||||
@ -275,34 +161,318 @@ pub fn check(dom:&rbx_dom_weak::WeakDom)->CheckReport{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MapStart must exist && there must be exactly one of any bonus start zones.
|
Ok(ModelInfo{
|
||||||
if counts.mode_start_counts.get(&ModeID::MAIN)==Some(&1)
|
model_class:model_instance.class.as_str(),
|
||||||
&&counts.mode_start_counts.iter().all(|(_,&c)|c==1){
|
model_name:model_instance.name.as_str(),
|
||||||
report.exactly_one_mapstart=Check::Pass;
|
map_info,
|
||||||
}
|
counts,
|
||||||
// iterate over start zones
|
})
|
||||||
if counts.mode_start_counts.iter().all(|(mode_id,_)|
|
}
|
||||||
// ensure that at least one end zone exists with the same mode id
|
|
||||||
counts.mode_finish_counts.get(mode_id).is_some_and(|&num|0<num)
|
|
||||||
){
|
|
||||||
report.at_least_one_mapfinish=Check::Pass;
|
|
||||||
}
|
|
||||||
// Spawn1 must exist
|
|
||||||
if counts.spawn_counts.get(&SpawnID::FIRST).is_some(){
|
|
||||||
report.spawn1_exists=Check::Pass;
|
|
||||||
}
|
|
||||||
if counts.spawn_counts.iter().all(|(_,&c)|c==1){
|
|
||||||
report.no_duplicate_spawns=Check::Pass;
|
|
||||||
}
|
|
||||||
if counts.wormhole_out_counts.iter().all(|(_,&c)|c==1){
|
|
||||||
report.no_duplicate_wormhole_out=Check::Pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
report
|
// check if an observed string matches and expected string
|
||||||
|
pub struct StringCheck<'a,T,Str>(Result<T,StringCheckContext<'a,Str>>);
|
||||||
|
pub struct StringCheckContext<'a,Str>{
|
||||||
|
observed:&'a str,
|
||||||
|
expected:Str,
|
||||||
|
}
|
||||||
|
impl<'a,Str> StringCheckContext<'a,Str>
|
||||||
|
where
|
||||||
|
&'a str:PartialEq<Str>,
|
||||||
|
{
|
||||||
|
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
|
||||||
|
if self.observed==self.expected{
|
||||||
|
StringCheck(Ok(value))
|
||||||
|
}else{
|
||||||
|
StringCheck(Err(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a,Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'a,Str>{
|
||||||
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
write!(f,"expected: {}, observed: {}",self.expected,self.observed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a string is empty
|
||||||
|
pub struct StringEmpty;
|
||||||
|
pub struct StringEmptyCheck<Context>(Result<Context,StringEmpty>);
|
||||||
|
impl std::fmt::Display for StringEmpty{
|
||||||
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
write!(f,"Empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for duplicate objects
|
||||||
|
pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>);
|
||||||
|
pub struct DuplicateCheck<ID>(Result<(),DuplicateCheckContext<ID>>);
|
||||||
|
impl<ID> DuplicateCheckContext<ID>{
|
||||||
|
fn check(self)->DuplicateCheck<ID>{
|
||||||
|
let Self(mut set)=self;
|
||||||
|
// remove correct entries
|
||||||
|
set.retain(|_,&mut c|c!=1);
|
||||||
|
// if any entries remain, they are incorrect
|
||||||
|
if set.is_empty(){
|
||||||
|
DuplicateCheck(Ok(()))
|
||||||
|
}else{
|
||||||
|
DuplicateCheck(Err(Self(set)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that there is at least one
|
||||||
|
pub struct AtLeastOneMatchingAndNoExtraCheckContext<ID>{
|
||||||
|
extra:HashMap<ID,u64>,
|
||||||
|
missing:HashSet<ID>,
|
||||||
|
}
|
||||||
|
pub struct AtLeastOneMatchingAndNoExtraCheck<ID>(Result<(),AtLeastOneMatchingAndNoExtraCheckContext<ID>>);
|
||||||
|
impl<ID> AtLeastOneMatchingAndNoExtraCheckContext<ID>{
|
||||||
|
fn new(initial_set:HashMap<ID,u64>)->Self{
|
||||||
|
Self{
|
||||||
|
extra:initial_set,
|
||||||
|
missing:HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<ID:Copy+Eq+std::hash::Hash> AtLeastOneMatchingAndNoExtraCheckContext<ID>{
|
||||||
|
fn check<T>(self,reference_set:&HashMap<ID,T>)->AtLeastOneMatchingAndNoExtraCheck<ID>{
|
||||||
|
let Self{mut extra,mut missing}=self;
|
||||||
|
// remove correct entries
|
||||||
|
for (id,_) in reference_set{
|
||||||
|
if extra.remove(id).is_none(){
|
||||||
|
// the set did not contain a required item. This is a fail
|
||||||
|
missing.insert(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if any entries remain, they are incorrect
|
||||||
|
if extra.is_empty()&&missing.is_empty(){
|
||||||
|
AtLeastOneMatchingAndNoExtraCheck(Ok(()))
|
||||||
|
}else{
|
||||||
|
AtLeastOneMatchingAndNoExtraCheck(Err(Self{extra,missing}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MapInfoOwned{
|
||||||
|
pub display_name:String,
|
||||||
|
pub creator:String,
|
||||||
|
pub game_id:GameID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// crazy!
|
||||||
|
pub struct MapCheck<'a>{
|
||||||
|
model_class:StringCheck<'a,(),&'a str>,
|
||||||
|
model_name:StringCheck<'a,(),String>,
|
||||||
|
display_name:Result<StringEmptyCheck<StringCheck<'a,&'a str,String>>,StringValueError>,
|
||||||
|
creator:Result<StringEmptyCheck<&'a str>,StringValueError>,
|
||||||
|
game_id:Result<GameID,ParseGameIDError>,
|
||||||
|
mapstart:Result<(),()>,
|
||||||
|
mode_start_counts:DuplicateCheck<ModeID>,
|
||||||
|
mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck<ModeID>,
|
||||||
|
spawn1:Result<(),()>,
|
||||||
|
spawn_counts:DuplicateCheck<SpawnID>,
|
||||||
|
wormhole_out_counts:DuplicateCheck<WormholeOutID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ModelInfo<'a>{
|
||||||
|
fn check(self)->MapCheck<'a>{
|
||||||
|
let model_class=StringCheckContext{
|
||||||
|
observed:self.model_class,
|
||||||
|
expected:"Model",
|
||||||
|
}.check(());
|
||||||
|
|
||||||
|
let model_name=StringCheckContext{
|
||||||
|
observed:self.model_name,
|
||||||
|
expected:self.model_name.to_snake_case(),
|
||||||
|
}.check(());
|
||||||
|
|
||||||
|
// check display name
|
||||||
|
let display_name=self.map_info.display_name.map(|display_name|{
|
||||||
|
if display_name.is_empty(){
|
||||||
|
StringEmptyCheck(Err(StringEmpty))
|
||||||
|
}else{
|
||||||
|
StringEmptyCheck(Ok(StringCheckContext{
|
||||||
|
observed:display_name,
|
||||||
|
expected:display_name.to_title_case(),
|
||||||
|
}.check(display_name)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// check Creator
|
||||||
|
let creator=self.map_info.creator.map(|creator|{
|
||||||
|
if creator.is_empty(){
|
||||||
|
StringEmptyCheck(Err(StringEmpty))
|
||||||
|
}else{
|
||||||
|
StringEmptyCheck(Ok(creator))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// check GameID
|
||||||
|
let game_id=self.map_info.game_id;
|
||||||
|
|
||||||
|
// MapStart must exist
|
||||||
|
let mapstart=if self.counts.mode_start_counts.get(&ModeID::MAIN).is_some(){
|
||||||
|
Ok(())
|
||||||
|
}else{
|
||||||
|
Err(())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn1 must exist
|
||||||
|
let spawn1=if self.counts.spawn_counts.get(&SpawnID::FIRST).is_some(){
|
||||||
|
Ok(())
|
||||||
|
}else{
|
||||||
|
Err(())
|
||||||
|
};
|
||||||
|
|
||||||
|
// check that at least one end zone exists for each start zone.
|
||||||
|
let mode_finish_counts=AtLeastOneMatchingAndNoExtraCheckContext::new(self.counts.mode_finish_counts)
|
||||||
|
.check(&self.counts.mode_start_counts);
|
||||||
|
|
||||||
|
// there must be exactly one start zone for every mode in the map.
|
||||||
|
let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check();
|
||||||
|
|
||||||
|
// there must be exactly one of any perticular spawn id in the map.
|
||||||
|
let spawn_counts=DuplicateCheckContext(self.counts.spawn_counts).check();
|
||||||
|
|
||||||
|
// there must be exactly one of any perticular wormhole out id in the map.
|
||||||
|
let wormhole_out_counts=DuplicateCheckContext(self.counts.wormhole_out_counts).check();
|
||||||
|
|
||||||
|
MapCheck{
|
||||||
|
model_class,
|
||||||
|
model_name,
|
||||||
|
display_name,
|
||||||
|
creator,
|
||||||
|
game_id,
|
||||||
|
mapstart,
|
||||||
|
mode_start_counts,
|
||||||
|
mode_finish_counts,
|
||||||
|
spawn1,
|
||||||
|
spawn_counts,
|
||||||
|
wormhole_out_counts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MapCheck<'a>{
|
||||||
|
fn pass(self)->Result<MapInfoOwned,Self>{
|
||||||
|
match self{
|
||||||
|
MapCheck{
|
||||||
|
model_class:StringCheck(Ok(())),
|
||||||
|
model_name:StringCheck(Ok(())),
|
||||||
|
display_name:Ok(StringEmptyCheck(Ok(StringCheck(Ok(display_name))))),
|
||||||
|
creator:Ok(StringEmptyCheck(Ok(creator))),
|
||||||
|
game_id:Ok(game_id),
|
||||||
|
mapstart:Ok(()),
|
||||||
|
mode_start_counts:DuplicateCheck(Ok(())),
|
||||||
|
mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck(Ok(())),
|
||||||
|
spawn1:Ok(()),
|
||||||
|
spawn_counts:DuplicateCheck(Ok(())),
|
||||||
|
wormhole_out_counts:DuplicateCheck(Ok(())),
|
||||||
|
}=>{
|
||||||
|
Ok(MapInfoOwned{
|
||||||
|
display_name:display_name.to_owned(),
|
||||||
|
creator:creator.to_owned(),
|
||||||
|
game_id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
other=>Err(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comma_separated<T,F>(f:&mut std::fmt::Formatter<'_>,mut it:impl Iterator<Item=T>,custom_write:F)->std::fmt::Result
|
||||||
|
where
|
||||||
|
F:Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result,
|
||||||
|
{
|
||||||
|
if let Some(t)=it.next(){
|
||||||
|
custom_write(f,t)?;
|
||||||
|
for t in it{
|
||||||
|
write!(f,", ")?;
|
||||||
|
custom_write(f,t)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> std::fmt::Display for MapCheck<'a>{
|
||||||
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
|
if let StringCheck(Err(context))=&self.model_class{
|
||||||
|
writeln!(f,"Invalid Model Class: {context}")?;
|
||||||
|
}
|
||||||
|
if let StringCheck(Err(context))=&self.model_name{
|
||||||
|
writeln!(f,"Invalid Model Name: {context}")?;
|
||||||
|
}
|
||||||
|
match &self.display_name{
|
||||||
|
Ok(StringEmptyCheck(Ok(StringCheck(Ok(_)))))=>(),
|
||||||
|
Ok(StringEmptyCheck(Ok(StringCheck(Err(context)))))=>writeln!(f,"Invalid DisplayName: {context}")?,
|
||||||
|
Ok(StringEmptyCheck(Err(context)))=>writeln!(f,"Invalid DisplayName: {context}")?,
|
||||||
|
Err(StringValueError::ObjectNotFound)=>writeln!(f,"Missing DisplayName StringValue")?,
|
||||||
|
Err(StringValueError::ValueNotSet)=>writeln!(f,"DisplayName Value not set")?,
|
||||||
|
Err(StringValueError::NonStringValue)=>writeln!(f,"DisplayName Value is not a String")?,
|
||||||
|
}
|
||||||
|
match &self.creator{
|
||||||
|
Ok(StringEmptyCheck(Ok(_)))=>(),
|
||||||
|
Ok(StringEmptyCheck(Err(context)))=>writeln!(f,"Invalid Creator: {context}")?,
|
||||||
|
Err(StringValueError::ObjectNotFound)=>writeln!(f,"Missing Creator StringValue")?,
|
||||||
|
Err(StringValueError::ValueNotSet)=>writeln!(f,"Creator Value not set")?,
|
||||||
|
Err(StringValueError::NonStringValue)=>writeln!(f,"Creator Value is not a String")?,
|
||||||
|
}
|
||||||
|
if let Err(_parse_game_id_error)=&self.game_id{
|
||||||
|
writeln!(f,"Model name must be prefixed with bhop_ surf_ or flytrials_")?;
|
||||||
|
}
|
||||||
|
if let Err(())=&self.mapstart{
|
||||||
|
writeln!(f,"Model has no MapStart")?;
|
||||||
|
}
|
||||||
|
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.mode_start_counts{
|
||||||
|
write!(f,"Duplicate start zones: ")?;
|
||||||
|
comma_separated(f,context.iter(),|f,(mode_id,count)|{
|
||||||
|
mode_id.write_start_zone(f)?;
|
||||||
|
write!(f,"({count} duplicates)")?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
writeln!(f,"")?;
|
||||||
|
}
|
||||||
|
if let AtLeastOneMatchingAndNoExtraCheck(Err(context))=&self.mode_finish_counts{
|
||||||
|
// perhaps there are extra end zones (context.extra)
|
||||||
|
if !context.extra.is_empty(){
|
||||||
|
write!(f,"Extra finish zones with no matching start zone: ")?;
|
||||||
|
comma_separated(f,context.extra.iter(),|f,(mode_id,_count)|
|
||||||
|
mode_id.write_finish_zone(f))?;
|
||||||
|
writeln!(f,"")?;
|
||||||
|
}
|
||||||
|
// perhaps there are missing end zones (context.missing)
|
||||||
|
if !context.missing.is_empty(){
|
||||||
|
write!(f,"Missing finish zones: ")?;
|
||||||
|
comma_separated(f,context.missing.iter(),|f,mode_id|
|
||||||
|
mode_id.write_finish_zone(f)
|
||||||
|
)?;
|
||||||
|
writeln!(f,"")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(())=&self.spawn1{
|
||||||
|
writeln!(f,"Model has no Spawn1")?;
|
||||||
|
}
|
||||||
|
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{
|
||||||
|
write!(f,"Duplicate spawn zones: ")?;
|
||||||
|
comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)|{
|
||||||
|
write!(f,"Spawn{spawn_id}({count} duplicates)")?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
writeln!(f,"")?;
|
||||||
|
}
|
||||||
|
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{
|
||||||
|
write!(f,"Duplicate wormhole out: ")?;
|
||||||
|
comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_out_id),count)|{
|
||||||
|
write!(f,"WormholeOut{wormhole_out_id}({count} duplicates)")?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
writeln!(f,"")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CheckReportAndVersion{
|
pub struct CheckReportAndVersion{
|
||||||
pub report:CheckReport,
|
pub status:Result<MapInfoOwned,String>,
|
||||||
pub version:u64,
|
pub version:u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,8 +499,15 @@ impl crate::message_handler::MessageHandler{
|
|||||||
// decode dom (slow!)
|
// decode dom (slow!)
|
||||||
let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?;
|
let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?;
|
||||||
|
|
||||||
let report=check(&dom);
|
// extract information from the model
|
||||||
|
let model_info=get_model_info(&dom).map_err(Error::GetRootInstance)?;
|
||||||
|
|
||||||
Ok(CheckReportAndVersion{report,version})
|
// convert the model information into a structured report
|
||||||
|
let map_check=model_info.check();
|
||||||
|
|
||||||
|
// check the report, generate an error message if it fails the check
|
||||||
|
let status=map_check.pass().map_err(|e|e.to_string());
|
||||||
|
|
||||||
|
Ok(CheckReportAndVersion{status,version})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,23 +21,27 @@ impl crate::message_handler::MessageHandler{
|
|||||||
|
|
||||||
// update the mapfix depending on the result
|
// update the mapfix depending on the result
|
||||||
match check_result{
|
match check_result{
|
||||||
Ok(CheckReportAndVersion{report,version})=>{
|
Ok(CheckReportAndVersion{status,version})=>{
|
||||||
if report.pass(){
|
match status{
|
||||||
// update the mapfix model status to submitted
|
// update the mapfix model status to submitted
|
||||||
|
Ok(map_info)=>
|
||||||
self.api.action_mapfix_submitted(
|
self.api.action_mapfix_submitted(
|
||||||
submissions_api::types::ActionMapfixSubmittedRequest{
|
submissions_api::types::ActionMapfixSubmittedRequest{
|
||||||
MapfixID:mapfix_id,
|
MapfixID:mapfix_id,
|
||||||
ModelVersion:version,
|
ModelVersion:version,
|
||||||
|
DisplayName:map_info.display_name,
|
||||||
|
Creator:map_info.creator,
|
||||||
|
GameID:map_info.game_id as u32,
|
||||||
}
|
}
|
||||||
).await.map_err(Error::ApiActionMapfixCheck)?;
|
).await.map_err(Error::ApiActionMapfixCheck)?,
|
||||||
}else{
|
|
||||||
// update the mapfix model status to request changes
|
// update the mapfix model status to request changes
|
||||||
|
Err(report)=>
|
||||||
self.api.action_mapfix_request_changes(
|
self.api.action_mapfix_request_changes(
|
||||||
submissions_api::types::ActionMapfixRequestChangesRequest{
|
submissions_api::types::ActionMapfixRequestChangesRequest{
|
||||||
MapfixID:mapfix_id,
|
MapfixID:mapfix_id,
|
||||||
ErrorMessage:report.to_string(),
|
ErrorMessage:report,
|
||||||
}
|
}
|
||||||
).await.map_err(Error::ApiActionMapfixCheck)?;
|
).await.map_err(Error::ApiActionMapfixCheck)?,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e)=>{
|
Err(e)=>{
|
||||||
|
@ -21,23 +21,27 @@ impl crate::message_handler::MessageHandler{
|
|||||||
|
|
||||||
// update the submission depending on the result
|
// update the submission depending on the result
|
||||||
match check_result{
|
match check_result{
|
||||||
Ok(CheckReportAndVersion{report,version})=>{
|
Ok(CheckReportAndVersion{status,version})=>{
|
||||||
if report.pass(){
|
match status{
|
||||||
// update the submission model status to submitted
|
// update the submission model status to submitted
|
||||||
|
Ok(map_info)=>
|
||||||
self.api.action_submission_submitted(
|
self.api.action_submission_submitted(
|
||||||
submissions_api::types::ActionSubmissionSubmittedRequest{
|
submissions_api::types::ActionSubmissionSubmittedRequest{
|
||||||
SubmissionID:submission_id,
|
SubmissionID:submission_id,
|
||||||
ModelVersion:version,
|
ModelVersion:version,
|
||||||
|
DisplayName:map_info.display_name,
|
||||||
|
Creator:map_info.creator,
|
||||||
|
GameID:map_info.game_id as u32,
|
||||||
}
|
}
|
||||||
).await.map_err(Error::ApiActionSubmissionCheck)?;
|
).await.map_err(Error::ApiActionSubmissionCheck)?,
|
||||||
}else{
|
|
||||||
// update the submission model status to request changes
|
// update the submission model status to request changes
|
||||||
|
Err(report)=>
|
||||||
self.api.action_submission_request_changes(
|
self.api.action_submission_request_changes(
|
||||||
submissions_api::types::ActionSubmissionRequestChangesRequest{
|
submissions_api::types::ActionSubmissionRequestChangesRequest{
|
||||||
SubmissionID:submission_id,
|
SubmissionID:submission_id,
|
||||||
ErrorMessage:report.to_string(),
|
ErrorMessage:report,
|
||||||
}
|
}
|
||||||
).await.map_err(Error::ApiActionSubmissionCheck)?;
|
).await.map_err(Error::ApiActionSubmissionCheck)?,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e)=>{
|
Err(e)=>{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::download::download_asset_version;
|
use crate::download::download_asset_version;
|
||||||
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,ParseGameIDError};
|
use crate::rbx_util::{get_root_instance,get_mapinfo,read_dom,MapInfo,ReadDomError,GetRootInstanceError,GameID};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -9,7 +9,6 @@ pub enum Error{
|
|||||||
Download(crate::download::Error),
|
Download(crate::download::Error),
|
||||||
ModelFileDecode(ReadDomError),
|
ModelFileDecode(ReadDomError),
|
||||||
GetRootInstance(GetRootInstanceError),
|
GetRootInstance(GetRootInstanceError),
|
||||||
ParseGameID(ParseGameIDError),
|
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for Error{
|
impl std::fmt::Display for Error{
|
||||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||||
@ -24,10 +23,10 @@ pub struct CreateRequest{
|
|||||||
}
|
}
|
||||||
#[allow(nonstandard_style)]
|
#[allow(nonstandard_style)]
|
||||||
pub struct CreateResult{
|
pub struct CreateResult{
|
||||||
pub AssetOwner:i64,
|
pub AssetOwner:u64,
|
||||||
pub DisplayName:String,
|
pub DisplayName:Option<String>,
|
||||||
pub Creator:String,
|
pub Creator:Option<String>,
|
||||||
pub GameID:i32,
|
pub GameID:Option<GameID>,
|
||||||
pub AssetVersion:u64,
|
pub AssetVersion:u64,
|
||||||
}
|
}
|
||||||
impl crate::message_handler::MessageHandler{
|
impl crate::message_handler::MessageHandler{
|
||||||
@ -63,13 +62,11 @@ impl crate::message_handler::MessageHandler{
|
|||||||
game_id,
|
game_id,
|
||||||
}=get_mapinfo(&dom,model_instance);
|
}=get_mapinfo(&dom,model_instance);
|
||||||
|
|
||||||
let game_id=game_id.map_err(Error::ParseGameID)?;
|
|
||||||
|
|
||||||
Ok(CreateResult{
|
Ok(CreateResult{
|
||||||
AssetOwner:user_id as i64,
|
AssetOwner:user_id,
|
||||||
DisplayName:display_name.unwrap_or_default().to_owned(),
|
DisplayName:display_name.ok().map(ToOwned::to_owned),
|
||||||
Creator:creator.unwrap_or_default().to_owned(),
|
Creator:creator.ok().map(ToOwned::to_owned),
|
||||||
GameID:game_id as i32,
|
GameID:game_id.ok(),
|
||||||
AssetVersion:asset_version,
|
AssetVersion:asset_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,11 @@ impl crate::message_handler::MessageHandler{
|
|||||||
// call create on api
|
// call create on api
|
||||||
self.api.create_mapfix(submissions_api::types::CreateMapfixRequest{
|
self.api.create_mapfix(submissions_api::types::CreateMapfixRequest{
|
||||||
OperationID:create_info.OperationID,
|
OperationID:create_info.OperationID,
|
||||||
AssetOwner:create_request.AssetOwner,
|
AssetOwner:create_request.AssetOwner as i64,
|
||||||
DisplayName:create_request.DisplayName.as_str(),
|
DisplayName:create_request.DisplayName.as_deref().unwrap_or_default(),
|
||||||
Creator:create_request.Creator.as_str(),
|
Creator:create_request.Creator.as_deref().unwrap_or_default(),
|
||||||
GameID:create_request.GameID,
|
// not great TODO: make this great
|
||||||
|
GameID:create_request.GameID.unwrap_or(crate::rbx_util::GameID::Bhop) as i32,
|
||||||
AssetID:create_info.ModelID,
|
AssetID:create_info.ModelID,
|
||||||
AssetVersion:create_request.AssetVersion,
|
AssetVersion:create_request.AssetVersion,
|
||||||
TargetAssetID:create_info.TargetAssetID,
|
TargetAssetID:create_info.TargetAssetID,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::nats_types::CreateSubmissionRequest;
|
use crate::nats_types::CreateSubmissionRequest;
|
||||||
use crate::create::CreateRequest;
|
use crate::create::CreateRequest;
|
||||||
|
use crate::rbx_util::GameID;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -19,13 +20,29 @@ impl crate::message_handler::MessageHandler{
|
|||||||
let create_request=self.create_inner(CreateRequest{
|
let create_request=self.create_inner(CreateRequest{
|
||||||
ModelID:create_info.ModelID,
|
ModelID:create_info.ModelID,
|
||||||
}).await.map_err(Error::Create)?;
|
}).await.map_err(Error::Create)?;
|
||||||
|
|
||||||
|
// grab values from submission form, otherwise try to fill blanks from map data
|
||||||
|
let display_name=if create_info.DisplayName.is_empty(){
|
||||||
|
create_request.DisplayName.as_deref().unwrap_or_default()
|
||||||
|
}else{
|
||||||
|
create_info.DisplayName.as_str()
|
||||||
|
};
|
||||||
|
|
||||||
|
let creator=if create_info.Creator.is_empty(){
|
||||||
|
create_request.Creator.as_deref().unwrap_or_default()
|
||||||
|
}else{
|
||||||
|
create_info.Creator.as_str()
|
||||||
|
};
|
||||||
|
|
||||||
|
let game_id=create_info.GameID.try_into().ok().or(create_request.GameID).unwrap_or(GameID::Bhop);
|
||||||
|
|
||||||
// call create on api
|
// call create on api
|
||||||
self.api.create_submission(submissions_api::types::CreateSubmissionRequest{
|
self.api.create_submission(submissions_api::types::CreateSubmissionRequest{
|
||||||
OperationID:create_info.OperationID,
|
OperationID:create_info.OperationID,
|
||||||
AssetOwner:create_request.AssetOwner,
|
AssetOwner:create_request.AssetOwner as i64,
|
||||||
DisplayName:create_request.DisplayName.as_str(),
|
DisplayName:display_name,
|
||||||
Creator:create_request.Creator.as_str(),
|
Creator:creator,
|
||||||
GameID:create_request.GameID,
|
GameID:game_id as i32,
|
||||||
AssetID:create_info.ModelID,
|
AssetID:create_info.ModelID,
|
||||||
AssetVersion:create_request.AssetVersion,
|
AssetVersion:create_request.AssetVersion,
|
||||||
}).await.map_err(Error::ApiActionSubmissionCreate)?;
|
}).await.map_err(Error::ApiActionSubmissionCreate)?;
|
||||||
|
@ -10,6 +10,9 @@ pub struct CreateSubmissionRequest{
|
|||||||
// operation_id is passed back in the response message
|
// operation_id is passed back in the response message
|
||||||
pub OperationID:i32,
|
pub OperationID:i32,
|
||||||
pub ModelID:u64,
|
pub ModelID:u64,
|
||||||
|
pub DisplayName:String,
|
||||||
|
pub Creator:String,
|
||||||
|
pub GameID:u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(nonstandard_style)]
|
#[allow(nonstandard_style)]
|
||||||
|
@ -70,6 +70,18 @@ impl std::str::FromStr for GameID{
|
|||||||
return Err(ParseGameIDError);
|
return Err(ParseGameIDError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub struct GameIDError;
|
||||||
|
impl TryFrom<u32> for GameID{
|
||||||
|
type Error=GameIDError;
|
||||||
|
fn try_from(value:u32)->Result<Self,Self::Error>{
|
||||||
|
match value{
|
||||||
|
1=>Ok(GameID::Bhop),
|
||||||
|
2=>Ok(GameID::Surf),
|
||||||
|
5=>Ok(GameID::FlyTrials),
|
||||||
|
_=>Err(GameIDError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MapInfo<'a>{
|
pub struct MapInfo<'a>{
|
||||||
pub display_name:Result<&'a str,StringValueError>,
|
pub display_name:Result<&'a str,StringValueError>,
|
||||||
@ -77,6 +89,7 @@ pub struct MapInfo<'a>{
|
|||||||
pub game_id:Result<GameID,ParseGameIDError>,
|
pub game_id:Result<GameID,ParseGameIDError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum StringValueError{
|
pub enum StringValueError{
|
||||||
ObjectNotFound,
|
ObjectNotFound,
|
||||||
ValueNotSet,
|
ValueNotSet,
|
||||||
|
65
web/src/app/submit/_game.tsx
Normal file
65
web/src/app/submit/_game.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { FormControl, Select, InputLabel, MenuItem } from "@mui/material";
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import InputBase from '@mui/material/InputBase';
|
||||||
|
import React from "react";
|
||||||
|
import { SelectChangeEvent } from "@mui/material";
|
||||||
|
|
||||||
|
// TODO: Properly style everything instead of pasting 🤚
|
||||||
|
|
||||||
|
type GameSelectionProps = {
|
||||||
|
game: number;
|
||||||
|
setGame: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BootstrapInput = styled(InputBase)(({ theme }) => ({
|
||||||
|
'label + &': {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
},
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
backgroundColor: '#0000',
|
||||||
|
color: '#FFF',
|
||||||
|
border: '1px solid rgba(175, 175, 175, 0.66)',
|
||||||
|
fontSize: 16,
|
||||||
|
padding: '10px 26px 10px 12px',
|
||||||
|
transition: theme.transitions.create(['border-color', 'box-shadow']),
|
||||||
|
fontFamily: [
|
||||||
|
'-apple-system',
|
||||||
|
'BlinkMacSystemFont',
|
||||||
|
'"Segoe UI"',
|
||||||
|
'Roboto',
|
||||||
|
'"Helvetica Neue"',
|
||||||
|
'Arial',
|
||||||
|
'sans-serif',
|
||||||
|
'"Apple Color Emoji"',
|
||||||
|
'"Segoe UI Emoji"',
|
||||||
|
'"Segoe UI Symbol"',
|
||||||
|
].join(','),
|
||||||
|
'&:focus': {
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: '#80bdff',
|
||||||
|
boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function GameSelection({ game, setGame }: GameSelectionProps) {
|
||||||
|
const handleChange = (event: SelectChangeEvent) => {
|
||||||
|
setGame(Number(event.target.value)); // TODO: Change later!! there's 100% a proper way of doing this
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel sx={{ color: "#646464" }}>Game</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={String(game)}
|
||||||
|
label="Game"
|
||||||
|
onChange={handleChange}
|
||||||
|
input={<BootstrapInput />}
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>Bhop</MenuItem>
|
||||||
|
<MenuItem value={2}>Surf</MenuItem>
|
||||||
|
<MenuItem value={3}>Fly Trials</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -2,19 +2,25 @@
|
|||||||
|
|
||||||
import { Button, TextField } from "@mui/material"
|
import { Button, TextField } from "@mui/material"
|
||||||
|
|
||||||
|
import GameSelection from "./_game";
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import Webpage from "@/app/_components/webpage"
|
import Webpage from "@/app/_components/webpage"
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import "./(styles)/page.scss"
|
import "./(styles)/page.scss"
|
||||||
|
|
||||||
interface SubmissionPayload {
|
interface SubmissionPayload {
|
||||||
AssetID: number;
|
AssetID: number;
|
||||||
|
DisplayName: string;
|
||||||
|
Creator: string;
|
||||||
|
GameID: number;
|
||||||
}
|
}
|
||||||
interface IdResponse {
|
interface IdResponse {
|
||||||
OperationID: number;
|
OperationID: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SubmissionInfoPage() {
|
export default function SubmissionInfoPage() {
|
||||||
|
const [game, setGame] = useState(1);
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -23,6 +29,9 @@ export default function SubmissionInfoPage() {
|
|||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
|
|
||||||
const payload: SubmissionPayload = {
|
const payload: SubmissionPayload = {
|
||||||
|
DisplayName: (formData.get("display-name") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||||
|
Creator: (formData.get("creator") as string) ?? "unknown", // TEMPORARY! TODO: Change
|
||||||
|
GameID: game,
|
||||||
AssetID: Number((formData.get("asset-id") as string) ?? "0"),
|
AssetID: Number((formData.get("asset-id") as string) ?? "0"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +73,10 @@ export default function SubmissionInfoPage() {
|
|||||||
<span className="spacer form-spacer"></span>
|
<span className="spacer form-spacer"></span>
|
||||||
</header>
|
</header>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<TextField className="form-field" id="asset-id" name="asset-id" label="Asset ID" variant="outlined"/>
|
<TextField className="form-field" id="asset-id" name="asset-id" label="Asset ID (required)" variant="outlined"/>
|
||||||
|
<TextField className="form-field" id="display-name" name="display-name" label="Display Name" variant="outlined"/>
|
||||||
|
<TextField className="form-field" id="creator" name="creator" label="Creator" variant="outlined"/>
|
||||||
|
<GameSelection game={game} setGame={setGame} />
|
||||||
<span className="spacer form-spacer"></span>
|
<span className="spacer form-spacer"></span>
|
||||||
<Button type="submit" variant="contained" startIcon={<SendIcon/>} sx={{
|
<Button type="submit" variant="contained" startIcon={<SendIcon/>} sx={{
|
||||||
width: "400px",
|
width: "400px",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user