feat(tiers): EmbeddedAI on-chain — base_multiplier + subsidy piso EmbeddedArm (#16)
Espejea HardwareTier::EmbeddedAI (Jetson Orin Nano) en el mirror de la cadena, en lockstep con arkhe-core bbac1b2 (Ergon Fase 1 EmbeddedAI, ADR-0072 / DC 2ba235e1). Trampa de la fuente preservada byte-fiel: EmbeddedAI APPEND discriminante 4 (SCALE wire-safe, attestations runtime_tier:u8 viejas válidas) PERO rank de capacidad 1 (entre EmbeddedArm=0 y Portable=2). Por eso Ord es MANUAL por rank() (no derive — un derive pondría EmbeddedAI > Ultra, falso) y el gating Hero es por rank, no por discriminante. - tiers.rs: variant EmbeddedAI (last) + Ord/PartialOrd manual por rank() + rank()/ordinal()/from_ordinal() SSOT (el reverse-map que evita perder el tier-floor al deserializar, #16) + base_multiplier_x100=10 (piso, el estudiante en Jetson no paga más que en Pi 5) + surcharge Hero gated=0 + supports_hero=false. - fee_schedule.rs: 6 arms EmbeddedAI = piso EmbeddedArm en subsidy_basis_points + educational override incluye EmbeddedAI (device de estudiante). - fees.rs / unified.rs: cobertura — compute_fee(EmbeddedAI)==EmbeddedArm + grid de paridad OCAFB-5 extendido a EmbeddedAI. Tests: 21 suites / 261 workspace verde, 0 regresión. SCALE index == ordinal sellado. FIX-4 (verificador attestation) ortogonal — no mapea el enum, intacto. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
fcf768b046
commit
ca50938495
4 files changed
+236
-13
No files matched your search
@@ -87,6 +87,11 @@ pub const fn subsidy_basis_points(hw: HardwareTier, action: ChainAction) -> u32
|
||||
(EmbeddedArm, Write) => 9000,
|
||||
(EmbeddedArm, Compute) => 10000, // educational compute free
|
||||
(EmbeddedArm, Transfer) => 9000,
|
||||
// EmbeddedAI (Jetson Orin Nano): MISMO piso EmbeddedArm — Inmutable #16,
|
||||
// el estudiante en Jetson no paga más que en Pi 5. Ergon bbac1b2.
|
||||
(EmbeddedAI, Write) => 9000,
|
||||
(EmbeddedAI, Compute) => 10000, // educational compute free
|
||||
(EmbeddedAI, Transfer) => 9000,
|
||||
// Portable: subsidy media
|
||||
(Portable, Write) => 7500,
|
||||
(Portable, Compute) => 5000,
|
||||
@@ -108,6 +113,10 @@ pub const fn subsidy_basis_points(hw: HardwareTier, action: ChainAction) -> u32
|
||||
(EmbeddedArm, DeployContract) => 9000, // Pi 5 dev contracts canonical
|
||||
(EmbeddedArm, CallContract) => 10000, // educational call free
|
||||
(EmbeddedArm, UploadContractCode) => 9000, // Pi 5 storage subsidy
|
||||
// EmbeddedAI (Jetson): mismo piso EmbeddedArm en contract actions (#16).
|
||||
(EmbeddedAI, DeployContract) => 9000, // Jetson dev contracts = piso Pi 5
|
||||
(EmbeddedAI, CallContract) => 10000, // educational call free
|
||||
(EmbeddedAI, UploadContractCode) => 9000, // Jetson storage subsidy = piso
|
||||
(Portable, DeployContract) => 7500,
|
||||
(Portable, CallContract) => 5000,
|
||||
(Portable, UploadContractCode) => 7500,
|
||||
@@ -247,11 +256,12 @@ impl OitFeeSchedule for OitFeeScheduleMock {
|
||||
}
|
||||
|
||||
fn subsidy_eligible(&self, _user_id: &str, action: ChainAction) -> bool {
|
||||
// OCAFB-3 override: educational_world → 100% subsidy EmbeddedArm + Portable
|
||||
// OCAFB-3 override: educational_world → 100% subsidy en los tiers floor
|
||||
// EmbeddedArm + EmbeddedAI + Portable (Jetson = device de estudiante, #16).
|
||||
if self.educational_world
|
||||
&& matches!(
|
||||
self.default_hardware,
|
||||
HardwareTier::EmbeddedArm | HardwareTier::Portable
|
||||
HardwareTier::EmbeddedArm | HardwareTier::EmbeddedAI | HardwareTier::Portable
|
||||
)
|
||||
{
|
||||
return true;
|
||||
@@ -260,11 +270,11 @@ impl OitFeeSchedule for OitFeeScheduleMock {
|
||||
}
|
||||
|
||||
fn tier_multiplier(&self, _user_id: &str, action: ChainAction) -> u64 {
|
||||
// Educational override → multiplier 0 (free)
|
||||
// Educational override → multiplier 0 (free) en los tiers floor.
|
||||
if self.educational_world
|
||||
&& matches!(
|
||||
self.default_hardware,
|
||||
HardwareTier::EmbeddedArm | HardwareTier::Portable
|
||||
HardwareTier::EmbeddedArm | HardwareTier::EmbeddedAI | HardwareTier::Portable
|
||||
)
|
||||
{
|
||||
return 0;
|
||||
@@ -295,6 +305,7 @@ mod tests {
|
||||
// Inmutable #37 baseline: Read NEVER charged per HardwareTier
|
||||
for hw in [
|
||||
HardwareTier::EmbeddedArm,
|
||||
HardwareTier::EmbeddedAI,
|
||||
HardwareTier::Portable,
|
||||
HardwareTier::Desktop,
|
||||
HardwareTier::Ultra,
|
||||
@@ -303,6 +314,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_ai_subsidy_mirrors_embedded_arm_floor_inmutable_16() {
|
||||
// El estudiante en Jetson (EmbeddedAI) no paga MÁS que en Pi 5
|
||||
// (EmbeddedArm): piso idéntico en TODA acción. Ergon bbac1b2, #16.
|
||||
for action in [
|
||||
ChainAction::Read,
|
||||
ChainAction::Write,
|
||||
ChainAction::Compute,
|
||||
ChainAction::Transfer,
|
||||
ChainAction::DeployContract,
|
||||
ChainAction::CallContract,
|
||||
ChainAction::UploadContractCode,
|
||||
] {
|
||||
assert_eq!(
|
||||
subsidy_basis_points(HardwareTier::EmbeddedAI, action),
|
||||
subsidy_basis_points(HardwareTier::EmbeddedArm, action),
|
||||
"EmbeddedAI debe espejar el piso EmbeddedArm en {action:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocafb_3_ultra_pays_full_fee_canonical() {
|
||||
// Ultra: no subsidy except Read
|
||||
|
||||
@@ -396,8 +396,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::fee_schedule::compute_fee as vendor_compute_fee;
|
||||
|
||||
const ALL_HW: [HardwareTier; 4] = [
|
||||
const ALL_HW: [HardwareTier; 5] = [
|
||||
HardwareTier::EmbeddedArm,
|
||||
HardwareTier::EmbeddedAI,
|
||||
HardwareTier::Portable,
|
||||
HardwareTier::Desktop,
|
||||
HardwareTier::Ultra,
|
||||
|
||||
@@ -105,6 +105,24 @@ mod tests {
|
||||
assert_eq!(fee.as_base_units(), Ogc::from_whole(1).as_base_units());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_ai_pays_same_floor_as_embedded_arm_inmutable_16() {
|
||||
// El estudiante en Jetson (EmbeddedAI) paga el MISMO piso 0.1× que en
|
||||
// Pi 5 (EmbeddedArm): mismo base_multiplier (10) + Hero gated (0). #16.
|
||||
let ai = compute_fee(
|
||||
Ogc::from_whole(10),
|
||||
HardwareTier::EmbeddedAI,
|
||||
AssetQualityTier::Standard,
|
||||
);
|
||||
let arm = compute_fee(
|
||||
Ogc::from_whole(10),
|
||||
HardwareTier::EmbeddedArm,
|
||||
AssetQualityTier::Standard,
|
||||
);
|
||||
assert_eq!(ai.as_base_units(), arm.as_base_units());
|
||||
assert_eq!(ai.as_base_units(), Ogc::from_whole(1).as_base_units());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_pays_three_tenths() {
|
||||
let fee = compute_fee(
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
/// Per Inmutable #16, fees scale inversely with tier so global access is preserved:
|
||||
/// a Pi 5 user in Sub-Saharan Africa pays 0.1x what a Vision Pro user pays for the
|
||||
/// same operation.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
///
|
||||
/// **`PartialOrd`/`Ord` se implementan MANUALMENTE** (vía [`HardwareTier::rank`]),
|
||||
/// NO derivados — espejo byte-fiel de `arkhe-core::HardwareTier`. Con el APPEND de
|
||||
/// `EmbeddedAI` (discriminante 4), un `Ord` derivado ordenaría por posición de
|
||||
/// declaración y pondría `EmbeddedAI > Ultra` — semánticamente FALSO (es un tier
|
||||
/// floor con-IA). El orden de capacidad vive en `rank()`; el discriminante de wire
|
||||
/// vive en `ordinal()`. Confundirlos = perder el tier-floor (#16).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)]
|
||||
#[derive(parity_scale_codec::DecodeWithMemTracking)]
|
||||
#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)]
|
||||
@@ -29,23 +34,106 @@ pub enum HardwareTier {
|
||||
Desktop,
|
||||
/// Vision Pro, high-end workstations — multiplier 0.7× (discount). Hero +0.3×.
|
||||
Ultra,
|
||||
/// Jetson Orin Nano class — embedded ARM **con aceleración IA** (Ampere +
|
||||
/// Tensor Cores) y memoria UNIFICADA. Floor con-IA que complementa
|
||||
/// `EmbeddedArm` (Pi 5): mismo piso de fee (0.1×, Inmutable #16 — el
|
||||
/// estudiante en Jetson no paga más que en Pi 5). Hero gated out.
|
||||
///
|
||||
/// **APPEND ordinal 4** (serde por-nombre + SCALE wire-safe: ningún ordinal
|
||||
/// existente cambia, attestations `runtime_tier: u8` viejas válidas). Su
|
||||
/// **capacidad NO es la del discriminante**: ver [`HardwareTier::rank`] (=1,
|
||||
/// entre `EmbeddedArm`=0 y `Portable`=2). ADR-0072 / DC `2ba235e1`. Espejo de
|
||||
/// `arkhe-core` commit `bbac1b2` (Ergon Fase 1 EmbeddedAI).
|
||||
EmbeddedAI,
|
||||
}
|
||||
|
||||
impl PartialOrd for HardwareTier {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HardwareTier {
|
||||
/// Orden por **capacidad** ([`HardwareTier::rank`]), no por discriminante de
|
||||
/// declaración. Espejo de `arkhe-core::HardwareTier`.
|
||||
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||
self.rank().cmp(&other.rank())
|
||||
}
|
||||
}
|
||||
|
||||
impl HardwareTier {
|
||||
/// Rango de **capacidad** (desacoplado del discriminante de declaración).
|
||||
/// `EmbeddedAI` (Jetson) se inserta entre `EmbeddedArm` y `Portable`: tiene IA
|
||||
/// pero su techo de memoria unificada lo limita. Úsese para TODO gating de
|
||||
/// capacidad (Hero, presupuestos); NUNCA `ordinal()`. Espejo de `arkhe-core`.
|
||||
#[inline]
|
||||
pub const fn rank(self) -> u8 {
|
||||
match self {
|
||||
HardwareTier::EmbeddedArm => 0,
|
||||
HardwareTier::EmbeddedAI => 1,
|
||||
HardwareTier::Portable => 2,
|
||||
HardwareTier::Desktop => 3,
|
||||
HardwareTier::Ultra => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Discriminante de declaración (serde por-nombre + SCALE wire-format).
|
||||
/// **NO es capacidad** — para gating usar [`HardwareTier::rank`]. `EmbeddedAI`
|
||||
/// = 4 por APPEND (wire-safe: ordinales existentes intactos). Coincide con el
|
||||
/// índice SCALE del enum (declaración) por construcción. Espejo de `arkhe-core`.
|
||||
#[inline]
|
||||
pub const fn ordinal(self) -> u8 {
|
||||
match self {
|
||||
HardwareTier::EmbeddedArm => 0,
|
||||
HardwareTier::Portable => 1,
|
||||
HardwareTier::Desktop => 2,
|
||||
HardwareTier::Ultra => 3,
|
||||
HardwareTier::EmbeddedAI => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverso de [`HardwareTier::ordinal`] — la **fuente única** de
|
||||
/// `u8 ordinal → HardwareTier`. `None` para un ordinal desconocido
|
||||
/// (forward-compat con futuros APPEND). Todo consumidor que interprete un
|
||||
/// tier `u8` (p.ej. `runtime_tier` de una attestation) DEBE delegar aquí en
|
||||
/// vez de duplicar la tabla: una tabla reverse duplicada fue cómo un variant
|
||||
/// nuevo (`EmbeddedAI`=4) se mapeaba a `None` y perdía el tier-floor al
|
||||
/// deserializar (Inmutable #16). Sellado por `ordinal_round_trips_all_variants`.
|
||||
#[inline]
|
||||
pub const fn from_ordinal(ordinal: u8) -> Option<HardwareTier> {
|
||||
match ordinal {
|
||||
0 => Some(HardwareTier::EmbeddedArm),
|
||||
1 => Some(HardwareTier::Portable),
|
||||
2 => Some(HardwareTier::Desktop),
|
||||
3 => Some(HardwareTier::Ultra),
|
||||
4 => Some(HardwareTier::EmbeddedAI),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Base fee multiplier scaled by 100 (avoids float).
|
||||
/// `EmbeddedArm` = 10 means 0.10× the base fee, `Desktop` = 100 means 1.00×.
|
||||
#[inline]
|
||||
pub const fn base_multiplier_x100(self) -> u32 {
|
||||
match self {
|
||||
HardwareTier::EmbeddedArm => 10,
|
||||
// EmbeddedAI (Jetson): piso EmbeddedArm (0.10×) — Inmutable #16: el
|
||||
// estudiante en Jetson no paga más que en Pi 5. Ergon `bbac1b2`.
|
||||
HardwareTier::EmbeddedAI => 10,
|
||||
HardwareTier::Portable => 30,
|
||||
HardwareTier::Desktop => 100,
|
||||
HardwareTier::Ultra => 70,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if this tier can transact `AssetQualityTier::Hero` assets. EmbeddedArm
|
||||
/// and Portable are gated out (Hero requires more VRAM than these tiers ship).
|
||||
/// True if this tier can transact `AssetQualityTier::Hero` assets. EmbeddedArm,
|
||||
/// EmbeddedAI and Portable are gated out (Hero requires more VRAM/unified
|
||||
/// memory than these tiers ship). El gating es por **capacidad** ([`rank`]):
|
||||
/// `EmbeddedAI` (rank 1) NO pasa Hero aunque su discriminante sea 4 — espejo
|
||||
/// de `arkhe-core::HardwareTier::asset_quality_tier` (Hero gated por memoria
|
||||
/// unificada, no por GPU).
|
||||
///
|
||||
/// [`rank`]: HardwareTier::rank
|
||||
#[inline]
|
||||
pub const fn supports_hero(self) -> bool {
|
||||
matches!(self, HardwareTier::Desktop | HardwareTier::Ultra)
|
||||
@@ -75,9 +163,11 @@ impl AssetQualityTier {
|
||||
(AssetQualityTier::Standard, _) => 0,
|
||||
(AssetQualityTier::Hero, HardwareTier::Desktop) => 50,
|
||||
(AssetQualityTier::Hero, HardwareTier::Ultra) => 30,
|
||||
// Hero gated out for EmbeddedArm + Portable — surcharge 0 because the
|
||||
// operation should be rejected upstream by the runtime, not charged.
|
||||
// Hero gated out for EmbeddedArm + EmbeddedAI + Portable (rank < Desktop)
|
||||
// — surcharge 0 because the operation should be rejected upstream by the
|
||||
// runtime, not charged.
|
||||
(AssetQualityTier::Hero, HardwareTier::EmbeddedArm) => 0,
|
||||
(AssetQualityTier::Hero, HardwareTier::EmbeddedAI) => 0,
|
||||
(AssetQualityTier::Hero, HardwareTier::Portable) => 0,
|
||||
}
|
||||
}
|
||||
@@ -130,6 +220,8 @@ mod tests {
|
||||
#[test]
|
||||
fn multipliers_match_chn_8_canonical() {
|
||||
assert_eq!(HardwareTier::EmbeddedArm.base_multiplier_x100(), 10);
|
||||
// EmbeddedAI = piso EmbeddedArm (0.10×, Inmutable #16). Ergon bbac1b2.
|
||||
assert_eq!(HardwareTier::EmbeddedAI.base_multiplier_x100(), 10);
|
||||
assert_eq!(HardwareTier::Portable.base_multiplier_x100(), 30);
|
||||
assert_eq!(HardwareTier::Desktop.base_multiplier_x100(), 100);
|
||||
assert_eq!(HardwareTier::Ultra.base_multiplier_x100(), 70);
|
||||
@@ -138,11 +230,85 @@ mod tests {
|
||||
#[test]
|
||||
fn hero_gating_canonical() {
|
||||
assert!(!HardwareTier::EmbeddedArm.supports_hero());
|
||||
// EmbeddedAI (rank 1 < Desktop 3) NO pasa Hero, pese a discriminante 4.
|
||||
assert!(!HardwareTier::EmbeddedAI.supports_hero());
|
||||
assert!(!HardwareTier::Portable.supports_hero());
|
||||
assert!(HardwareTier::Desktop.supports_hero());
|
||||
assert!(HardwareTier::Ultra.supports_hero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tier_ordering_by_capability_rank() {
|
||||
// Ord MANUAL vía rank() — EmbeddedAI se inserta entre EmbeddedArm y
|
||||
// Portable, NUNCA por encima de Ultra (lo que haría el derive(Ord) roto).
|
||||
assert!(HardwareTier::EmbeddedArm < HardwareTier::EmbeddedAI);
|
||||
assert!(HardwareTier::EmbeddedAI < HardwareTier::Portable);
|
||||
assert!(HardwareTier::Portable < HardwareTier::Desktop);
|
||||
assert!(HardwareTier::Desktop < HardwareTier::Ultra);
|
||||
// El anti-test de la trampa: EmbeddedAI (discriminante 4) NO es > Ultra.
|
||||
assert!(HardwareTier::EmbeddedAI < HardwareTier::Ultra);
|
||||
// rank explícito.
|
||||
assert_eq!(HardwareTier::EmbeddedArm.rank(), 0);
|
||||
assert_eq!(HardwareTier::EmbeddedAI.rank(), 1);
|
||||
assert_eq!(HardwareTier::Portable.rank(), 2);
|
||||
assert_eq!(HardwareTier::Desktop.rank(), 3);
|
||||
assert_eq!(HardwareTier::Ultra.rank(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinal_round_trips_all_variants() {
|
||||
// SSOT del mapeo u8→tier: from_ordinal(ordinal()) == Some(self) para TODOS
|
||||
// los variants. Caza el bug #16: un APPEND que olvide from_ordinal mapearía
|
||||
// el tier nuevo a None y perdería el floor al deserializar.
|
||||
for t in [
|
||||
HardwareTier::EmbeddedArm,
|
||||
HardwareTier::Portable,
|
||||
HardwareTier::Desktop,
|
||||
HardwareTier::Ultra,
|
||||
HardwareTier::EmbeddedAI,
|
||||
] {
|
||||
assert_eq!(HardwareTier::from_ordinal(t.ordinal()), Some(t));
|
||||
}
|
||||
// Ordinales existentes intactos (attestations viejas runtime_tier: u8).
|
||||
assert_eq!(HardwareTier::EmbeddedArm.ordinal(), 0);
|
||||
assert_eq!(HardwareTier::Portable.ordinal(), 1);
|
||||
assert_eq!(HardwareTier::Desktop.ordinal(), 2);
|
||||
assert_eq!(HardwareTier::Ultra.ordinal(), 3);
|
||||
assert_eq!(HardwareTier::EmbeddedAI.ordinal(), 4, "APPEND wire-safe");
|
||||
// Forward-compat: ordinal desconocido → None.
|
||||
assert_eq!(HardwareTier::from_ordinal(5), None);
|
||||
assert_eq!(HardwareTier::from_ordinal(255), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_ai_scale_index_matches_ordinal() {
|
||||
use parity_scale_codec::{Decode, Encode};
|
||||
// SCALE codifica por índice de declaración: EmbeddedAI declarado último =
|
||||
// índice 4 = ordinal(). Garantiza que el wire on-chain coincide con el u8.
|
||||
for t in [
|
||||
HardwareTier::EmbeddedArm,
|
||||
HardwareTier::Portable,
|
||||
HardwareTier::Desktop,
|
||||
HardwareTier::Ultra,
|
||||
HardwareTier::EmbeddedAI,
|
||||
] {
|
||||
let enc = t.encode();
|
||||
assert_eq!(enc.len(), 1, "fieldless enum = 1 byte {t:?}");
|
||||
assert_eq!(enc[0], t.ordinal(), "SCALE index debe = ordinal {t:?}");
|
||||
let back = HardwareTier::decode(&mut &enc[..]).unwrap();
|
||||
assert_eq!(back, t);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_ai_serde_name_stable() {
|
||||
// serde por-nombre — wire de payloads guardados estable cross-version.
|
||||
let s = serde_json::to_string(&HardwareTier::EmbeddedAI).unwrap();
|
||||
assert_eq!(s, "\"EmbeddedAI\"");
|
||||
let back: HardwareTier = serde_json::from_str(&s).unwrap();
|
||||
assert_eq!(back, HardwareTier::EmbeddedAI);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hero_surcharge_canonical_chn_8() {
|
||||
assert_eq!(
|
||||
@@ -159,6 +325,7 @@ mod tests {
|
||||
fn standard_surcharge_always_zero() {
|
||||
for hw in [
|
||||
HardwareTier::EmbeddedArm,
|
||||
HardwareTier::EmbeddedAI,
|
||||
HardwareTier::Portable,
|
||||
HardwareTier::Desktop,
|
||||
HardwareTier::Ultra,
|
||||
@@ -169,11 +336,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hero_surcharge_zero_for_gated_tiers() {
|
||||
// Hero on EmbeddedArm/Portable returns 0 because rejection is upstream.
|
||||
// Hero on EmbeddedArm/EmbeddedAI/Portable returns 0 because rejection is
|
||||
// upstream (rank < Desktop).
|
||||
assert_eq!(
|
||||
AssetQualityTier::Hero.surcharge_x100(HardwareTier::EmbeddedArm),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
AssetQualityTier::Hero.surcharge_x100(HardwareTier::EmbeddedAI),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
AssetQualityTier::Hero.surcharge_x100(HardwareTier::Portable),
|
||||
0
|
||||
|
||||
Reference in new issue
Block a user