Compare commits
	
		
			5 commits
		
	
	
		
			15b8ec562c
			...
			2b8d3942da
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2b8d3942da | ||
|  | 5b8ec05589 | ||
|  | d4505e58d4 | ||
|  | 1ab11164d3 | ||
|  | 9511f49163 | 
|  | @ -157,24 +157,9 @@ pub mod rust { | |||
|                 .packages | ||||
|                 .iter() | ||||
|                 .flat_map(|(_, pkg)| { | ||||
|                     pkg.types.values().map(|ty| { | ||||
|                         let name = match ty { | ||||
|                             UnrealType::Class(class) => { | ||||
|                                 format!("U{}", canonicalize_name(&class.name)) | ||||
|                             } | ||||
|                             UnrealType::Struct(class) => { | ||||
|                                 format!("F{}", canonicalize_name(&class.name)) | ||||
|                             } | ||||
|                             UnrealType::Actor(class) => { | ||||
|                                 format!("A{}", canonicalize_name(&class.name)) | ||||
|                             } | ||||
|                             UnrealType::Enum(class) => { | ||||
|                                 format!("E{}", canonicalize_name(&class.name)) | ||||
|                             } | ||||
|                         }; | ||||
| 
 | ||||
|                         (ty.obj_ref(), name) | ||||
|                     }) | ||||
|                     pkg.types | ||||
|                         .values() | ||||
|                         .map(|ty| (ty.obj_ref(), Self::get_prefixed_name(&ty))) | ||||
|                 }) | ||||
|                 .collect::<BTreeMap<_, _>>(); | ||||
| 
 | ||||
|  | @ -184,6 +169,38 @@ pub mod rust { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// returns the absolute path of a type with the assumption that all
 | ||||
|         /// packages are children of the path `::crate::sdk`
 | ||||
|         fn get_type_path(&self, key: &ObjectRef) -> Option<String> { | ||||
|             let pkg = &self.sdk.packages.get(&key.package)?.name; | ||||
|             self.get_type_name(key) | ||||
|                 .map(|name| format!("::crate::sdk::{pkg}::{name}")) | ||||
|         } | ||||
| 
 | ||||
|         /// returns the precached, prefixed and cannonicalized (for this
 | ||||
|         /// language, Rust) name for this object-ref
 | ||||
|         fn get_type_name(&self, key: &ObjectRef) -> Option<String> { | ||||
|             self.type_name_cache.get(key).cloned() | ||||
|         } | ||||
| 
 | ||||
|         /// prefixes the typename according to its kind (UObject, AActor, FStruct, EEnum)
 | ||||
|         fn get_prefixed_name(ty: &UnrealType) -> String { | ||||
|             match ty { | ||||
|                 UnrealType::Class(_) => { | ||||
|                     format!("U{}", canonicalize_name(&ty.unique_name())) | ||||
|                 } | ||||
|                 UnrealType::Struct(_) => { | ||||
|                     format!("F{}", canonicalize_name(&ty.unique_name())) | ||||
|                 } | ||||
|                 UnrealType::Actor(_) => { | ||||
|                     format!("A{}", canonicalize_name(&ty.unique_name())) | ||||
|                 } | ||||
|                 UnrealType::Enum(_) => { | ||||
|                     format!("E{}", canonicalize_name(&ty.unique_name())) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         pub fn build(self) -> anyhow::Result<()> { | ||||
|             for pkg in self.sdk.packages.values() { | ||||
|                 self.generate_package(pkg)?; | ||||
|  | @ -195,64 +212,61 @@ pub mod rust { | |||
|         fn type_name(&self, ty: &Type) -> anyhow::Result<String> { | ||||
|             let type_name = match ty { | ||||
|                 Type::Ptr(inner) | Type::Ref(inner) => { | ||||
|                     format!("Option<NonNull<{}>>", self.type_name(&inner)?) | ||||
|                     format!( | ||||
|                         "::core::option::Option<NonNull<{}>>", | ||||
|                         self.type_name(&inner)? | ||||
|                     ) | ||||
|                 } | ||||
|                 Type::WeakPtr(inner) => { | ||||
|                     format!( | ||||
|                         "TWeakObjectPtr<{}>", | ||||
|                         self.type_name_cache | ||||
|                             .get(inner) | ||||
|                         "::crate::engine::TWeakObjectPtr<{}>", | ||||
|                         self.get_type_path(inner) | ||||
|                             .context("type name was not cached.")? | ||||
|                     ) | ||||
|                 } | ||||
|                 Type::SoftPtr(inner) => { | ||||
|                     format!( | ||||
|                         "TSoftObjectPtr<{}>", | ||||
|                         self.type_name_cache | ||||
|                             .get(inner) | ||||
|                         "::crate::engine::TSoftObjectPtr<{}>", | ||||
|                         self.get_type_path(inner) | ||||
|                             .context("type name was not cached.")? | ||||
|                     ) | ||||
|                 } | ||||
|                 Type::LazyPtr(inner) => { | ||||
|                     format!( | ||||
|                         "TLazyObjectPtr<{}>", | ||||
|                         self.type_name_cache | ||||
|                             .get(inner) | ||||
|                         "::crate::engine::TLazyObjectPtr<{}>", | ||||
|                         self.get_type_path(inner) | ||||
|                             .context("type name was not cached.")? | ||||
|                     ) | ||||
|                 } | ||||
|                 Type::AssetPtr(inner) => format!( | ||||
|                     "TAssetPtr<{}>", | ||||
|                     self.type_name_cache | ||||
|                         .get(inner) | ||||
|                     "::crate::engine::TAssetPtr<{}>", | ||||
|                     self.get_type_path(inner) | ||||
|                         .context("type name was not cached.")? | ||||
|                 ), | ||||
|                 Type::Array(inner) => format!("TArray<{}>", self.type_name(&inner)?), | ||||
|                 Type::Array(inner) => { | ||||
|                     format!("::crate::engine::TArray<{}>", self.type_name(&inner)?) | ||||
|                 } | ||||
|                 Type::Primitive(prim) => { | ||||
|                     format!("{prim}") | ||||
|                 } | ||||
|                 Type::RawArray { ty, len } => { | ||||
|                     format!("[{}; {}]", self.type_name(&ty)?, len) | ||||
|                 } | ||||
|                 Type::Name => "FName".to_string(), | ||||
|                 Type::String => "FString".to_string(), | ||||
|                 Type::Text => "FText".to_string(), | ||||
|                 Type::Name => "::crate::engine::FName".to_string(), | ||||
|                 Type::String => "::crate::engine::FString".to_string(), | ||||
|                 Type::Text => "::crate::engine::FText".to_string(), | ||||
|                 Type::Enum { enum_type, .. } => self | ||||
|                     .type_name_cache | ||||
|                     .get(enum_type) | ||||
|                     .context("type name was not cached.")? | ||||
|                     .clone(), | ||||
|                     .get_type_path(enum_type) | ||||
|                     .context("type name was not cached.")?, | ||||
|                 Type::Class(class) => { | ||||
|                     format!( | ||||
|                         "::core::option::Option<{}>", | ||||
|                         self.type_name_cache | ||||
|                             .get(class) | ||||
|                         self.get_type_path(class) | ||||
|                             .context("type name was not cached.")? | ||||
|                     ) | ||||
|                 } | ||||
|                 Type::Struct(class) => self | ||||
|                     .type_name_cache | ||||
|                     .get(class) | ||||
|                     .get_type_path(class) | ||||
|                     .context("type name was not cached.")? | ||||
|                     .clone(), | ||||
|             }; | ||||
|  | @ -262,8 +276,7 @@ pub mod rust { | |||
| 
 | ||||
|         fn generate_enum(&self, enum0: &Enum) -> anyhow::Result<TokenStream> { | ||||
|             let name = self | ||||
|                 .type_name_cache | ||||
|                 .get(&enum0.obj_ref) | ||||
|                 .get_type_name(&enum0.obj_ref) | ||||
|                 .context("enum name was not previously canonicalized and cached.")?; | ||||
| 
 | ||||
|             let variants = enum0.values.iter().map(|(&value, name)| { | ||||
|  | @ -411,13 +424,13 @@ pub mod rust { | |||
|             struct_name: &str, | ||||
|             method: &ClassMethod, | ||||
|         ) -> anyhow::Result<(TokenStream, TokenStream)> { | ||||
|             let method_name = canonicalize_name(&method.name); | ||||
|             let method_name = canonicalize_name(&method.unique_name()); | ||||
| 
 | ||||
|             let parameters = method | ||||
|                 .parameters | ||||
|                 .iter() | ||||
|                 .map(|parameter| { | ||||
|                     let name = canonicalize_name(¶meter.name); | ||||
|                     let name = canonicalize_name(¶meter.unique_name()); | ||||
|                     let type_name = self.type_name(¶meter.ty)?; | ||||
| 
 | ||||
|                     anyhow::Ok((parameter, name, type_name)) | ||||
|  | @ -577,14 +590,16 @@ pub mod rust { | |||
|         fn generate_struct_ctor( | ||||
|             &self, | ||||
|             _class: &Class, | ||||
|             _name: &str, | ||||
|             type_name: &str, | ||||
|             fields: &Vec<(&ClassField, Cow<str>, String)>, | ||||
|         ) -> TokenStream { | ||||
|             let fields_defs = fields.iter().map(|(_, name, ty)| quote! {#name: #ty}); | ||||
| 
 | ||||
|             let this_field_asignments = fields.iter().map(|(_, name, _ty)| { | ||||
|                 let setter = format_ident!("set_{}", name); | ||||
|                 quote! {this.#setter(#name);} | ||||
|                 let field_trait = format_ident!("{type_name}Fields"); | ||||
| 
 | ||||
|                 quote! {<Self as #field_trait>::#setter(this, #name);} | ||||
|             }); | ||||
| 
 | ||||
|             // FIXME: handle super struct fields aswell, ARK doesnt seem to have those anyways.
 | ||||
|  | @ -611,7 +626,7 @@ pub mod rust { | |||
|                 .fields | ||||
|                 .iter() | ||||
|                 .map(|field| { | ||||
|                     let name = canonicalize_name(&field.name); | ||||
|                     let name = canonicalize_name(&field.unique_name()); | ||||
|                     let ty = self.type_name(&field.ty)?; | ||||
| 
 | ||||
|                     anyhow::Ok((field, name, ty)) | ||||
|  | @ -658,9 +673,8 @@ pub mod rust { | |||
|         } | ||||
| 
 | ||||
|         fn generate_class(&self, class: &Class) -> anyhow::Result<TokenStream> { | ||||
|             let name = self | ||||
|                 .type_name_cache | ||||
|                 .get(&class.obj_ref) | ||||
|             let name = &self | ||||
|                 .get_type_name(&class.obj_ref) | ||||
|                 .context("enum name was not previously canonicalized and cached.")?; | ||||
| 
 | ||||
|             let (field_trait, ctor) = self.generate_struct_fields(class, name)?; | ||||
|  | @ -684,8 +698,6 @@ pub mod rust { | |||
|                 impl #method_trait for #name {} | ||||
|             }; | ||||
| 
 | ||||
|             // TODO: impl super-type fields and methods.
 | ||||
| 
 | ||||
|             let mut sup = class.super_class; | ||||
|             let super_traits = core::iter::from_fn(|| { | ||||
|                 if let Some(key) = sup { | ||||
|  | @ -696,13 +708,12 @@ pub mod rust { | |||
|                     None | ||||
|                 } | ||||
|             }) | ||||
|             .map(|ty| ty.unique_name()) | ||||
|             // SAFETY: we already got this type by its obj_ref, so it must be there.
 | ||||
|             .map(|ty| self.get_type_path(&ty.obj_ref()).unwrap()) | ||||
|             .map(|super_name| { | ||||
|                 let fields = format_ident!("{super_name}Fields"); | ||||
|                 let methods = format_ident!("{super_name}Methods"); | ||||
| 
 | ||||
|                 // FIXME: full path here? meaning I need the package name aswell.
 | ||||
| 
 | ||||
|                 quote! { | ||||
|                     impl #fields for #name {} | ||||
|                     impl #methods for #name {} | ||||
|  | @ -729,8 +740,7 @@ pub mod rust { | |||
|         } | ||||
| 
 | ||||
|         fn generate_package(&self, pkg: &ProcessedPackage) -> anyhow::Result<()> { | ||||
|             // TODO: canonicalize_name(&pkg.name);
 | ||||
|             let pkg_name = "PACKAGE_NAME_PLACEHOLDER".to_string(); | ||||
|             let pkg_name = canonicalize_name(&pkg.name); | ||||
| 
 | ||||
|             for (_id, ty) in &pkg.types { | ||||
|                 let tokens = match ty { | ||||
|  |  | |||
|  | @ -16,3 +16,4 @@ pub mod v2_types; | |||
| 
 | ||||
| pub mod any_type; | ||||
| pub mod types; | ||||
| pub mod util; | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ use ron::ser::PrettyConfig; | |||
| 
 | ||||
| use crate::global_tables::objects::GOBJECTS; | ||||
| use crate::sdk::repr::*; | ||||
| use crate::util::DedupIter; | ||||
| use crate::v2_types::{ | ||||
|     any_type::{self, AnyField, AnyObject, AnyProperty, AnyStruct}, | ||||
|     traits::{ | ||||
|  | @ -238,6 +239,28 @@ impl Class { | |||
|                 }, | ||||
|             ); | ||||
| 
 | ||||
|         let fields = fields | ||||
|             .into_iter() | ||||
|             .dedup_with_by( | ||||
|                 |n, field| ClassField { | ||||
|                     override_name: Some(format!("{}{}", field.name, n)), | ||||
|                     ..field | ||||
|                 }, | ||||
|                 |field: &ClassField| -> String { field.name.clone() }, | ||||
|             ) | ||||
|             .collect(); | ||||
| 
 | ||||
|         let methods = methods | ||||
|             .into_iter() | ||||
|             .dedup_with_by( | ||||
|                 |n, field| ClassMethod { | ||||
|                     override_name: Some(format!("{}{}", field.name, n)), | ||||
|                     ..field | ||||
|                 }, | ||||
|                 |field: &ClassMethod| -> String { field.name.clone() }, | ||||
|             ) | ||||
|             .collect(); | ||||
| 
 | ||||
|         (fields, methods) | ||||
|     } | ||||
| 
 | ||||
|  | @ -257,14 +280,16 @@ impl Class { | |||
| 
 | ||||
| impl ClassField { | ||||
|     pub fn from_uprop(prop: UProperty) -> anyhow::Result<Self> { | ||||
|         let name = prop | ||||
|             .name() | ||||
|             .get_name() | ||||
|             .context("failed to retrieve field name")?; | ||||
|         Ok(Self { | ||||
|             offset: *prop.offset() as u32, | ||||
|             size: *prop.element_size() as u32, | ||||
|             flags: *prop.property_flags(), | ||||
|             name: prop | ||||
|                 .name() | ||||
|                 .get_name() | ||||
|                 .context("failed to retrieve field name")?, | ||||
|             override_name: None, | ||||
|             name, | ||||
|             ty: resolve_type(prop).context("failed to get field type")?, | ||||
|         }) | ||||
|     } | ||||
|  | @ -288,11 +313,21 @@ impl ClassMethod { | |||
|                 _ => None, | ||||
|             }) | ||||
|             .map(|prop| ClassField::from_uprop(prop)) | ||||
|             .collect::<anyhow::Result<Vec<_>>>()?; | ||||
|             .collect::<anyhow::Result<Vec<_>>>()? | ||||
|             .into_iter() | ||||
|             .dedup_with_by( | ||||
|                 |n, field| ClassField { | ||||
|                     override_name: Some(format!("{}{}", field.name, n)), | ||||
|                     ..field | ||||
|                 }, | ||||
|                 |field: &ClassField| -> String { field.name.clone() }, | ||||
|             ) | ||||
|             .collect(); | ||||
| 
 | ||||
|         Ok(ClassMethod { | ||||
|             name, | ||||
|             full_name, | ||||
|             override_name: None, | ||||
|             flags: *func.function_flags(), | ||||
|             parameters: params, | ||||
|         }) | ||||
|  | @ -359,6 +394,14 @@ impl Package { | |||
|                 .context("not actually a package object")?, | ||||
|             types, | ||||
|             dependencies, | ||||
|             name: self | ||||
|                 .package_object | ||||
|                 .get_name() | ||||
|                 .context("failed to get package object name.")?, | ||||
|             full_name: self | ||||
|                 .package_object | ||||
|                 .get_full_name() | ||||
|                 .context("failed to get package object name.")?, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -55,6 +55,8 @@ pub struct Package { | |||
| pub struct ProcessedPackage { | ||||
|     /// the package object ref.
 | ||||
|     pub package_object: PackageRef, | ||||
|     pub name: String, | ||||
|     pub full_name: String, | ||||
|     /// all types extracted from this package referenced by their `ObjectRef`.
 | ||||
|     pub types: BTreeMap<ObjectRef, UnrealType>, | ||||
|     /// All other packages that types in this package depend on directly.
 | ||||
|  | @ -142,6 +144,7 @@ pub struct ClassField { | |||
|     pub offset: u32, | ||||
|     pub size: u32, | ||||
|     pub name: String, | ||||
|     pub override_name: Option<String>, | ||||
|     pub flags: EPropertyFlags, | ||||
|     pub ty: Type, | ||||
| } | ||||
|  | @ -166,11 +169,19 @@ impl ClassField { | |||
|     pub fn is_rep(&self) -> bool { | ||||
|         !self.flags.contains(EPropertyFlags::RepSkip) | ||||
|     } | ||||
| 
 | ||||
|     pub fn unique_name(&self) -> &str { | ||||
|         self.override_name | ||||
|             .as_ref() | ||||
|             .map(|s| s.as_str()) | ||||
|             .unwrap_or(self.name.as_str()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct ClassMethod { | ||||
|     pub name: String, | ||||
|     pub override_name: Option<String>, | ||||
|     pub full_name: String, | ||||
|     pub flags: EFunctionFlags, | ||||
|     pub parameters: Vec<ClassField>, | ||||
|  | @ -180,13 +191,23 @@ impl ClassMethod { | |||
|     pub fn is_static(&self) -> bool { | ||||
|         self.flags.contains(EFunctionFlags::Static) | ||||
|     } | ||||
| 
 | ||||
|     pub fn is_native(&self) -> bool { | ||||
|         self.flags.contains(EFunctionFlags::Native) | ||||
|     } | ||||
| 
 | ||||
|     /// this function is replicated to the server, might be exploitable.
 | ||||
|     pub fn is_net_server(&self) -> bool { | ||||
|         self.flags.contains(EFunctionFlags::NetServer) | ||||
|     } | ||||
| 
 | ||||
|     pub fn unique_name(&self) -> &str { | ||||
|         self.override_name | ||||
|             .as_ref() | ||||
|             .map(|s| s.as_str()) | ||||
|             .unwrap_or(self.name.as_str()) | ||||
|     } | ||||
| 
 | ||||
|     pub fn out_params(&self) -> Vec<&ClassField> { | ||||
|         self.parameters | ||||
|             .iter() | ||||
|  |  | |||
							
								
								
									
										210
									
								
								unreal-sdk/src/util.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								unreal-sdk/src/util.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,210 @@ | |||
| use std::collections::hash_map::Entry; | ||||
| use std::collections::HashMap; | ||||
| use std::hash::Hash; | ||||
| 
 | ||||
| pub struct DedupIdentity; | ||||
| 
 | ||||
| pub trait DedupGetHash<T> { | ||||
|     type Hashed: Hash + Eq; | ||||
|     fn get_hash<'a>(&mut self, a: &'a T) -> Self::Hashed; | ||||
| } | ||||
| 
 | ||||
| impl<T> DedupGetHash<T> for DedupIdentity | ||||
| where | ||||
|     T: Hash + Eq + Clone, | ||||
| { | ||||
|     type Hashed = T; | ||||
| 
 | ||||
|     fn get_hash<'a>(&mut self, a: &'a T) -> Self::Hashed { | ||||
|         a.clone() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<F, T, H> DedupGetHash<T> for F | ||||
| where | ||||
|     F: FnMut(&T) -> H, | ||||
|     H: Hash + Eq, | ||||
| { | ||||
|     type Hashed = H; | ||||
| 
 | ||||
|     fn get_hash<'a>(&mut self, a: &'a T) -> Self::Hashed { | ||||
|         self(a) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct DedupWithBy<I, F, H> | ||||
| where | ||||
|     I: Iterator, | ||||
|     F: FnMut(usize, <I as Iterator>::Item) -> <I as Iterator>::Item, | ||||
|     H: DedupGetHash<<I as Iterator>::Item>, | ||||
| { | ||||
|     iter: I, | ||||
|     /// makes item unique, is told how many elements of this equality.
 | ||||
|     cb_fn: F, | ||||
|     /// returns the borrowed element by which to compare the iterators elements
 | ||||
|     hash_fn: H, | ||||
|     cache: HashMap<H::Hashed, usize>, | ||||
| } | ||||
| 
 | ||||
| pub trait DedupIter: Iterator { | ||||
|     fn dedup_with<'a, F>(self, f: F) -> DedupWithBy<Self, F, DedupIdentity> | ||||
|     where | ||||
|         F: FnMut(usize, <Self as Iterator>::Item) -> <Self as Iterator>::Item, | ||||
|         Self: Sized, | ||||
|         <Self as Iterator>::Item: Eq + Hash + Clone, | ||||
|     { | ||||
|         DedupWithBy { | ||||
|             iter: self, | ||||
|             cb_fn: f, | ||||
|             hash_fn: DedupIdentity, | ||||
|             cache: HashMap::new(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn dedup_with_by<'a, F, H>(self, f: F, h: H) -> DedupWithBy<Self, F, H> | ||||
|     where | ||||
|         F: FnMut(usize, <Self as Iterator>::Item) -> <Self as Iterator>::Item, | ||||
|         H: DedupGetHash<<Self as Iterator>::Item>, | ||||
|         Self: Sized, | ||||
|     { | ||||
|         DedupWithBy { | ||||
|             iter: self, | ||||
|             cb_fn: f, | ||||
|             hash_fn: h, | ||||
|             cache: HashMap::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<I> DedupIter for I where I: Iterator {} | ||||
| 
 | ||||
| impl<I, F, H> Iterator for DedupWithBy<I, F, H> | ||||
| where | ||||
|     I: Iterator, | ||||
|     F: FnMut(usize, <I as Iterator>::Item) -> <I as Iterator>::Item, | ||||
|     H: DedupGetHash<<I as Iterator>::Item>, | ||||
| { | ||||
|     type Item = <I as Iterator>::Item; | ||||
| 
 | ||||
|     fn next(&mut self) -> Option<Self::Item> { | ||||
|         let mut next = self.iter.next()?; | ||||
| 
 | ||||
|         let next = loop { | ||||
|             let hash = self.hash_fn.get_hash(&next); | ||||
| 
 | ||||
|             match self.cache.entry(hash) { | ||||
|                 Entry::Occupied(mut entry) => { | ||||
|                     *entry.get_mut() += 1; | ||||
| 
 | ||||
|                     next = (self.cb_fn)(*entry.get(), next); | ||||
|                 } | ||||
|                 Entry::Vacant(entry) => { | ||||
|                     entry.insert(0); | ||||
|                     break next; | ||||
|                 } | ||||
|             }; | ||||
|         }; | ||||
| 
 | ||||
|         Some(next) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn numbers() { | ||||
|         let numbers = [1, 2, 2, 2, 3, 4, 5, 6, 22]; | ||||
| 
 | ||||
|         let unique = numbers | ||||
|             .into_iter() | ||||
|             .dedup_with(|n, i| (i * 10 + n) as usize) | ||||
|             .collect::<Vec<_>>(); | ||||
| 
 | ||||
|         assert_eq!(&unique, &[1, 2, 21, 22, 3, 4, 5, 6, 221]); | ||||
|     } | ||||
| 
 | ||||
|     #[derive(Debug, PartialEq, Clone)] | ||||
|     struct Key { | ||||
|         number: usize, | ||||
|         other: f32, | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn keys() { | ||||
|         let keys = [ | ||||
|             Key { | ||||
|                 number: 1, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 2, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 2, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 2, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 3, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 4, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 5, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|         ]; | ||||
|         let expected = [ | ||||
|             Key { | ||||
|                 number: 1, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 2, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 21, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 22, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 3, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 4, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|             Key { | ||||
|                 number: 5, | ||||
|                 other: 1.0, | ||||
|             }, | ||||
|         ]; | ||||
| 
 | ||||
|         let unique = keys | ||||
|             .into_iter() | ||||
|             .dedup_with_by( | ||||
|                 |n, key| Key { | ||||
|                     number: (key.number * 10 + n), | ||||
|                     ..key | ||||
|                 }, | ||||
|                 |key: &Key| -> usize { key.number }, | ||||
|             ) | ||||
|             .collect::<Vec<_>>(); | ||||
| 
 | ||||
|         assert_eq!(&unique, &expected); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in a new issue