feat: updated taiko calc

This commit is contained in:
MaxOhn
2024-09-28 19:14:47 +02:00
parent 3c57b9b409
commit 64001a3f8f
13 changed files with 248 additions and 191 deletions
+26
View File
@@ -5,6 +5,8 @@ use self::{
repeating_hit_patterns::RepeatingHitPatterns,
};
use super::object::{TaikoDifficultyObject, TaikoDifficultyObjects};
pub mod alternating_mono_pattern;
pub mod mono_streak;
pub mod preprocessor;
@@ -16,3 +18,27 @@ pub struct TaikoDifficultyColor {
pub alternating_mono_pattern: Option<Weak<AlternatingMonoPattern>>,
pub repeating_hit_patterns: Option<RefCount<RepeatingHitPatterns>>,
}
impl TaikoDifficultyColor {
pub fn previous_color_change<'a>(
&self,
hit_objects: &'a TaikoDifficultyObjects,
) -> Option<&'a RefCount<TaikoDifficultyObject>> {
self.mono_streak
.as_ref()
.and_then(Weak::upgrade)
.and_then(|mono| mono.get().first_hit_object())
.and_then(|h| hit_objects.previous_note(&h.get(), 0))
}
pub fn next_color_change<'a>(
&self,
hit_objects: &'a TaikoDifficultyObjects,
) -> Option<&'a RefCount<TaikoDifficultyObject>> {
self.mono_streak
.as_ref()
.and_then(Weak::upgrade)
.and_then(|mono| mono.get().last_hit_object())
.and_then(|h| hit_objects.next_note(&h.get(), 0))
}
}
@@ -35,4 +35,8 @@ impl MonoStreak {
pub fn first_hit_object(&self) -> Option<RefCount<TaikoDifficultyObject>> {
self.hit_objects.first().and_then(Weak::upgrade)
}
pub fn last_hit_object(&self) -> Option<RefCount<TaikoDifficultyObject>> {
self.hit_objects.last().and_then(Weak::upgrade)
}
}
+14 -14
View File
@@ -2,7 +2,7 @@ use std::collections::VecDeque;
use crate::{
taiko::difficulty::object::TaikoDifficultyObjects,
util::sync::{Ref, RefCount},
util::sync::{Ref, RefCount, Weak},
};
use super::{
@@ -17,11 +17,6 @@ impl ColorDifficultyPreprocessor {
let hit_patterns = Self::encode(hit_objects);
for repeating_hit_pattern in hit_patterns {
if let Some(obj) = repeating_hit_pattern.get().first_hit_object() {
obj.get_mut().color.repeating_hit_patterns =
Some(RefCount::clone(&repeating_hit_pattern));
}
let mono_patterns = Ref::map(repeating_hit_pattern.get(), |repeating| {
repeating.alternating_mono_patterns.as_slice()
});
@@ -33,11 +28,6 @@ impl ColorDifficultyPreprocessor {
mono_pattern.idx = i;
}
if let Some(obj) = mono_pattern.get().first_hit_object() {
obj.get_mut().color.alternating_mono_pattern =
Some(RefCount::downgrade(mono_pattern));
}
let mono_streaks = Ref::map(mono_pattern.get(), |alternating| {
alternating.mono_streaks.as_slice()
});
@@ -49,9 +39,19 @@ impl ColorDifficultyPreprocessor {
borrowed.idx = j;
}
if let Some(obj) = mono_streak.get().first_hit_object() {
obj.get_mut().color.mono_streak = Some(RefCount::downgrade(mono_streak));
};
for hit_object in mono_streak
.get()
.hit_objects
.iter()
.filter_map(Weak::upgrade)
{
let mut borrowed = hit_object.get_mut();
borrowed.color.repeating_hit_patterns =
Some(RefCount::clone(&repeating_hit_pattern));
borrowed.color.alternating_mono_pattern =
Some(RefCount::downgrade(mono_pattern));
borrowed.color.mono_streak = Some(RefCount::downgrade(mono_streak));
}
}
}
}
+24 -11
View File
@@ -1,6 +1,7 @@
use std::{cmp, mem, slice::Iter};
use crate::{
any::difficulty::skills::Skill,
model::{beatmap::HitWindows, hit_object::HitObject},
taiko::TaikoBeatmap,
util::sync::RefCount,
@@ -8,8 +9,9 @@ use crate::{
};
use super::{
combined_difficulty_value,
object::{TaikoDifficultyObject, TaikoDifficultyObjects},
skills::peaks::{Peaks, PeaksSkill},
skills::TaikoSkills,
DifficultyValues, TaikoDifficultyAttributes,
};
@@ -53,7 +55,7 @@ pub struct TaikoGradualDifficulty {
attrs: TaikoDifficultyAttributes,
diff_objects: TaikoDifficultyObjects,
diff_objects_iter: Iter<'static, RefCount<TaikoDifficultyObject>>,
peaks: Peaks,
skills: TaikoSkills,
total_hits: usize,
first_combos: FirstTwoCombos,
}
@@ -96,7 +98,7 @@ impl TaikoGradualDifficulty {
&mut n_diff_objects,
);
let peaks = Peaks::new();
let skills = TaikoSkills::new();
let attrs = TaikoDifficultyAttributes {
hit_window,
@@ -117,7 +119,7 @@ impl TaikoGradualDifficulty {
difficulty,
diff_objects,
diff_objects_iter,
peaks,
skills,
attrs,
total_hits,
first_combos,
@@ -144,7 +146,10 @@ impl Iterator for TaikoGradualDifficulty {
loop {
let curr = self.diff_objects_iter.next()?;
let borrowed = curr.get();
PeaksSkill::new(&mut self.peaks, &self.diff_objects).process(&borrowed);
Skill::new(&mut self.skills.rhythm, &self.diff_objects).process(&borrowed);
Skill::new(&mut self.skills.color, &self.diff_objects).process(&borrowed);
Skill::new(&mut self.skills.stamina, &self.diff_objects).process(&borrowed);
if borrowed.base_hit_type.is_hit() {
self.attrs.max_combo += 1;
@@ -166,10 +171,14 @@ impl Iterator for TaikoGradualDifficulty {
self.idx += 1;
let color = self.peaks.color_difficulty_value();
let rhythm = self.peaks.rhythm_difficulty_value();
let stamina = self.peaks.stamina_difficulty_value();
let combined = self.peaks.clone().difficulty_value();
let color = self.skills.color.as_difficulty_value();
let rhythm = self.skills.rhythm.as_difficulty_value();
let stamina = self.skills.stamina.as_difficulty_value();
let combined = combined_difficulty_value(
self.skills.color.clone(),
self.skills.rhythm.clone(),
self.skills.stamina.clone(),
);
let mut attrs = self.attrs.clone();
@@ -225,13 +234,17 @@ impl Iterator for TaikoGradualDifficulty {
}
}
let mut peaks = PeaksSkill::new(&mut self.peaks, &self.diff_objects);
let mut rhythm = Skill::new(&mut self.skills.rhythm, &self.diff_objects);
let mut color = Skill::new(&mut self.skills.color, &self.diff_objects);
let mut stamina = Skill::new(&mut self.skills.stamina, &self.diff_objects);
for _ in 0..take {
loop {
let curr = self.diff_objects_iter.next()?;
let borrowed = curr.get();
peaks.process(&borrowed);
rhythm.process(&borrowed);
color.process(&borrowed);
stamina.process(&borrowed);
if borrowed.base_hit_type.is_hit() {
self.attrs.max_combo += 1;
+86 -31
View File
@@ -1,16 +1,18 @@
use std::cmp;
use crate::{
any::difficulty::skills::Skill,
taiko::{
difficulty::{
color::preprocessor::ColorDifficultyPreprocessor,
object::{TaikoDifficultyObject, TaikoDifficultyObjects},
skills::peaks::PeaksSkill,
},
object::TaikoObject,
},
Difficulty,
};
use self::skills::peaks::Peaks;
use self::skills::{color::Color, rhythm::Rhythm, stamina::Stamina, TaikoSkills};
use super::{attributes::TaikoDifficultyAttributes, convert::TaikoBeatmap};
@@ -20,7 +22,10 @@ mod object;
mod rhythm;
mod skills;
const DIFFICULTY_MULTIPLIER: f64 = 1.35;
const DIFFICULTY_MULTIPLIER: f64 = 0.084_375;
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.2 * DIFFICULTY_MULTIPLIER;
const COLOR_SKILL_MULTIPLIER: f64 = 0.375 * DIFFICULTY_MULTIPLIER;
const STAMINA_SKILL_MULTIPLIER: f64 = 0.375 * DIFFICULTY_MULTIPLIER;
pub fn difficulty(
difficulty: &Difficulty,
@@ -32,7 +37,14 @@ pub fn difficulty(
.hit_windows()
.od;
let DifficultyValues { peaks, max_combo } = DifficultyValues::calculate(difficulty, converted);
let DifficultyValues {
skills: TaikoSkills {
rhythm,
color,
stamina,
},
max_combo,
} = DifficultyValues::calculate(difficulty, converted);
let mut attrs = TaikoDifficultyAttributes {
hit_window,
@@ -41,10 +53,10 @@ pub fn difficulty(
..Default::default()
};
let color_rating = peaks.color_difficulty_value();
let rhythm_rating = peaks.rhythm_difficulty_value();
let stamina_rating = peaks.stamina_difficulty_value();
let combined_rating = peaks.difficulty_value();
let color_rating = color.as_difficulty_value();
let rhythm_rating = rhythm.as_difficulty_value();
let stamina_rating = stamina.as_difficulty_value();
let combined_rating = combined_difficulty_value(color, rhythm, stamina);
DifficultyValues::eval(
&mut attrs,
@@ -57,6 +69,57 @@ pub fn difficulty(
attrs
}
fn combined_difficulty_value(color: Color, rhythm: Rhythm, stamina: Stamina) -> f64 {
fn norm(p: f64, values: [f64; 2]) -> f64 {
values
.into_iter()
.fold(0.0, |sum, x| sum + x.powf(p))
.powf(p.recip())
}
let color_peaks = color.get_curr_strain_peaks();
let rhythm_peaks = rhythm.get_curr_strain_peaks();
let stamina_peaks = stamina.get_curr_strain_peaks();
let cap = cmp::min(
cmp::min(color_peaks.len(), rhythm_peaks.len()),
stamina_peaks.len(),
);
let mut peaks = Vec::with_capacity(cap);
let iter = color_peaks
.iter()
.zip(rhythm_peaks.iter())
.zip(stamina_peaks.iter());
for ((mut color_peak, mut rhythm_peak), mut stamina_peak) in iter {
color_peak *= COLOR_SKILL_MULTIPLIER;
rhythm_peak *= RHYTHM_SKILL_MULTIPLIER;
stamina_peak *= STAMINA_SKILL_MULTIPLIER;
let mut peak = norm(1.5, [color_peak, stamina_peak]);
peak = norm(2.0, [peak, rhythm_peak]);
// * Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// * These sections will not contribute to the difficulty.
if peak > 0.0 {
peaks.push(peak);
}
}
let mut difficulty = 0.0;
let mut weight = 1.0;
peaks.sort_by(|a, b| b.total_cmp(a));
for strain in peaks {
difficulty += strain * weight;
weight *= 0.9;
}
difficulty
}
fn rescale(stars: f64) -> f64 {
if stars < 0.0 {
stars
@@ -66,7 +129,7 @@ fn rescale(stars: f64) -> f64 {
}
pub struct DifficultyValues {
pub peaks: Peaks,
pub skills: TaikoSkills,
pub max_combo: u32,
}
@@ -89,17 +152,21 @@ impl DifficultyValues {
// The first two hit objects have no difficulty object
n_diff_objects = n_diff_objects.saturating_sub(2);
let mut peaks = Peaks::new();
let mut skills = TaikoSkills::new();
{
let mut peaks = PeaksSkill::new(&mut peaks, &diff_objects);
let mut rhythm = Skill::new(&mut skills.rhythm, &diff_objects);
let mut color = Skill::new(&mut skills.color, &diff_objects);
let mut stamina = Skill::new(&mut skills.stamina, &diff_objects);
for hit_object in diff_objects.iter().take(n_diff_objects) {
peaks.process(&hit_object.get());
rhythm.process(&hit_object.get());
color.process(&hit_object.get());
stamina.process(&hit_object.get());
}
}
Self { peaks, max_combo }
Self { skills, max_combo }
}
pub fn eval(
@@ -107,26 +174,14 @@ impl DifficultyValues {
color_difficulty_value: f64,
rhythm_difficulty_value: f64,
stamina_difficulty_value: f64,
peaks_difficulty_value: f64,
combined_difficulty_value: f64,
) {
let color_rating = color_difficulty_value * DIFFICULTY_MULTIPLIER;
let rhythm_rating = rhythm_difficulty_value * DIFFICULTY_MULTIPLIER;
let stamina_rating = stamina_difficulty_value * DIFFICULTY_MULTIPLIER;
let combined_rating = peaks_difficulty_value * DIFFICULTY_MULTIPLIER;
let color_rating = color_difficulty_value * COLOR_SKILL_MULTIPLIER;
let rhythm_rating = rhythm_difficulty_value * RHYTHM_SKILL_MULTIPLIER;
let stamina_rating = stamina_difficulty_value * STAMINA_SKILL_MULTIPLIER;
let combined_rating = combined_difficulty_value;
let mut star_rating = rescale(combined_rating * 1.4);
// * TODO: This is temporary measure as we don't detect abuse of multiple-input
// * playstyles of converts within the current system.
if attrs.is_convert {
star_rating *= 0.925;
// * For maps with low colour variance and high stamina requirement,
// * multiple inputs are more likely to be abused.
if color_rating < 2.0 && stamina_rating > 8.0 {
star_rating *= 0.8;
}
}
let star_rating = rescale(combined_rating * 1.4);
attrs.stamina = stamina_rating;
attrs.rhythm = rhythm_rating;
+17 -3
View File
@@ -132,15 +132,23 @@ impl TaikoDifficultyObjects {
}
}
pub fn previous_note(
&self,
pub fn previous_note<'a>(
&'a self,
curr: &TaikoDifficultyObject,
backwards_idx: usize,
) -> Option<&RefCount<TaikoDifficultyObject>> {
) -> Option<&'a RefCount<TaikoDifficultyObject>> {
curr.note_idx
.checked_sub(backwards_idx + 1)
.and_then(|idx| self.note_objects.get(idx))
}
pub fn next_note<'a>(
&'a self,
curr: &TaikoDifficultyObject,
forwards_idx: usize,
) -> Option<&'a RefCount<TaikoDifficultyObject>> {
self.note_objects.get(curr.note_idx + (forwards_idx + 1))
}
}
#[rustfmt::skip]
@@ -180,3 +188,9 @@ impl IDifficultyObject for TaikoDifficultyObject {
self.idx
}
}
impl PartialEq for TaikoDifficultyObject {
fn eq(&self, other: &Self) -> bool {
self.idx == other.idx
}
}
+17 -4
View File
@@ -154,7 +154,11 @@ impl ColorEvaluator {
let mut difficulty = 0.0;
if let Some(mono_streak) = color.mono_streak.as_ref().and_then(Weak::upgrade) {
difficulty += Self::evaluate_diff_of_mono_streak(&mono_streak);
if let Some(first_hit_object) = mono_streak.get().first_hit_object() {
if &*first_hit_object.get() == hit_object {
difficulty += Self::evaluate_diff_of_mono_streak(&mono_streak);
}
}
}
if let Some(alternating_mono_pattern) = color
@@ -162,12 +166,21 @@ impl ColorEvaluator {
.as_ref()
.and_then(Weak::upgrade)
{
difficulty +=
Self::evaluate_diff_of_alternating_mono_pattern(&alternating_mono_pattern);
if let Some(first_hit_object) = alternating_mono_pattern.get().first_hit_object() {
if &*first_hit_object.get() == hit_object {
difficulty +=
Self::evaluate_diff_of_alternating_mono_pattern(&alternating_mono_pattern);
}
}
}
if let Some(repeating_hit_patterns) = color.repeating_hit_patterns.as_ref() {
difficulty += Self::evaluate_diff_of_repeating_hit_patterns(repeating_hit_patterns);
if let Some(first_hit_object) = repeating_hit_patterns.get().first_hit_object() {
if &*first_hit_object.get() == hit_object {
difficulty +=
Self::evaluate_diff_of_repeating_hit_patterns(repeating_hit_patterns);
}
}
}
difficulty
+18 -1
View File
@@ -1,4 +1,21 @@
use self::{color::Color, rhythm::Rhythm, stamina::Stamina};
pub mod color;
pub mod peaks;
pub mod rhythm;
pub mod stamina;
pub struct TaikoSkills {
pub rhythm: Rhythm,
pub color: Color,
pub stamina: Stamina,
}
impl TaikoSkills {
pub fn new() -> Self {
Self {
rhythm: Rhythm::default(),
color: Color::default(),
stamina: Stamina::default(),
}
}
}
-114
View File
@@ -1,114 +0,0 @@
use std::cmp;
use crate::{
any::difficulty::skills::Skill,
taiko::difficulty::object::{TaikoDifficultyObject, TaikoDifficultyObjects},
};
use super::{color::Color, rhythm::Rhythm, stamina::Stamina};
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.2 * FINAL_MULTIPLIER;
const COLOR_SKILL_MULTIPLIER: f64 = 0.375 * FINAL_MULTIPLIER;
const STAMINA_SKILL_MULTIPLIER: f64 = 0.375 * FINAL_MULTIPLIER;
const FINAL_MULTIPLIER: f64 = 0.0625;
#[derive(Clone)]
pub struct Peaks {
pub color: Color,
pub rhythm: Rhythm,
pub stamina: Stamina,
}
impl Peaks {
pub fn new() -> Self {
Self {
color: Color::default(),
rhythm: Rhythm::default(),
stamina: Stamina::default(),
}
}
pub fn color_difficulty_value(&self) -> f64 {
self.color.as_difficulty_value() * COLOR_SKILL_MULTIPLIER
}
pub fn rhythm_difficulty_value(&self) -> f64 {
self.rhythm.as_difficulty_value() * RHYTHM_SKILL_MULTIPLIER
}
pub fn stamina_difficulty_value(&self) -> f64 {
self.stamina.as_difficulty_value() * STAMINA_SKILL_MULTIPLIER
}
fn norm(p: f64, values: impl IntoIterator<Item = f64>) -> f64 {
values
.into_iter()
.fold(0.0, |sum, x| sum + x.powf(p))
.powf(p.recip())
}
pub fn difficulty_value(self) -> f64 {
let color_peaks = self.color.get_curr_strain_peaks();
let rhythm_peaks = self.rhythm.get_curr_strain_peaks();
let stamina_peaks = self.stamina.get_curr_strain_peaks();
let cap = cmp::min(
cmp::min(color_peaks.len(), rhythm_peaks.len()),
stamina_peaks.len(),
);
let mut peaks = Vec::with_capacity(cap);
let zip = color_peaks
.iter()
.zip(rhythm_peaks.iter())
.zip(stamina_peaks.iter());
for ((mut color_peak, mut rhythm_peak), mut stamina_peak) in zip {
color_peak *= COLOR_SKILL_MULTIPLIER;
rhythm_peak *= RHYTHM_SKILL_MULTIPLIER;
stamina_peak *= STAMINA_SKILL_MULTIPLIER;
let mut peak = Self::norm(1.5, [color_peak, stamina_peak]);
peak = Self::norm(2.0, [peak, rhythm_peak]);
if peak > 0.0 {
peaks.push(peak);
}
}
let mut difficulty = 0.0;
let mut weight = 1.0;
peaks.sort_by(|a, b| b.total_cmp(a));
for strain in peaks {
difficulty += strain * weight;
weight *= 0.9;
}
difficulty
}
}
pub struct PeaksSkill<'a> {
pub color: Skill<'a, Color>,
pub rhythm: Skill<'a, Rhythm>,
pub stamina: Skill<'a, Stamina>,
}
impl<'a> PeaksSkill<'a> {
pub fn new(peaks: &'a mut Peaks, diff_objects: &'a TaikoDifficultyObjects) -> Self {
Self {
color: Skill::new(&mut peaks.color, diff_objects),
rhythm: Skill::new(&mut peaks.rhythm, diff_objects),
stamina: Skill::new(&mut peaks.stamina, diff_objects),
}
}
pub fn process(&mut self, curr: &TaikoDifficultyObject) {
self.rhythm.process(curr);
self.color.process(curr);
self.stamina.process(curr);
}
}
+32 -7
View File
@@ -100,28 +100,53 @@ struct StaminaEvaluator;
impl StaminaEvaluator {
fn speed_bonus(mut interval: f64) -> f64 {
// * Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
// * Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
// * TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
interval = interval.max(50.0);
// * Interval is capped at a very small value to prevent infinite values.
interval = interval.max(1.0);
30.0 / interval
}
fn available_fingers_for(
hit_object: &TaikoDifficultyObject,
hit_objects: &TaikoDifficultyObjects,
) -> usize {
let prev_color_change = hit_object.color.previous_color_change(hit_objects);
if prev_color_change
.is_some_and(|change| hit_object.start_time - change.get().start_time < 300.0)
{
return 2;
}
let next_color_change = hit_object.color.next_color_change(hit_objects);
if next_color_change
.is_some_and(|change| change.get().start_time - hit_object.start_time < 300.0)
{
return 2;
}
4
}
fn evaluate_diff_of(curr: &TaikoDifficultyObject, hit_objects: &TaikoDifficultyObjects) -> f64 {
if matches!(curr.base_hit_type, HitType::NonHit) {
return 0.0;
}
// * Find the previous hit object hit by the current key, which is two notes of the same colour prior.
// * Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
// * available fingers.
let taiko_curr = curr;
let key_prev = hit_objects.previous_mono(taiko_curr, 1);
let key_prev = hit_objects.previous_mono(
taiko_curr,
Self::available_fingers_for(taiko_curr, hit_objects) - 1,
);
if let Some(key_prev) = key_prev {
// * Add a base strain to all objects
0.5 + Self::speed_bonus(taiko_curr.start_time - key_prev.get().start_time)
} else {
// * There is no previous hit object hit by the current key
// * There is no previous hit object hit by the current finger
0.0
}
}
+3 -3
View File
@@ -446,7 +446,7 @@ impl TaikoPerformanceInner<'_> {
diff_value *= 0.985;
}
if self.mods.hd() {
if self.mods.hd() && !self.attrs.is_convert {
diff_value *= 1.025;
}
@@ -476,8 +476,8 @@ impl TaikoPerformanceInner<'_> {
let len_bonus = (self.total_hits() / 1500.0).powf(0.3).min(1.15);
acc_value *= len_bonus;
// * Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
if self.mods.hd() && self.mods.fl() {
// * Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
if self.mods.hd() && self.mods.fl() && !self.attrs.is_convert {
acc_value *= (1.075 * len_bonus).max(1.05);
}
+3 -3
View File
@@ -24,8 +24,8 @@ pub fn strains(difficulty: &Difficulty, converted: &TaikoBeatmap<'_>) -> TaikoSt
let values = DifficultyValues::calculate(difficulty, converted);
TaikoStrains {
color: values.peaks.color.get_curr_strain_peaks().into_vec(),
rhythm: values.peaks.rhythm.get_curr_strain_peaks().into_vec(),
stamina: values.peaks.stamina.get_curr_strain_peaks().into_vec(),
color: values.skills.color.get_curr_strain_peaks().into_vec(),
rhythm: values.skills.rhythm.get_curr_strain_peaks().into_vec(),
stamina: values.skills.stamina.get_curr_strain_peaks().into_vec(),
}
}
+4
View File
@@ -6,8 +6,10 @@ pub use inner::*;
mod inner {
use std::{cell::RefCell, rc::Rc};
#[repr(transparent)]
pub struct RefCount<T>(pub(super) Rc<RefCell<T>>);
#[repr(transparent)]
pub struct Weak<T>(pub(super) std::rc::Weak<RefCell<T>>);
pub type Ref<'a, T> = std::cell::Ref<'a, T>;
@@ -45,8 +47,10 @@ mod inner {
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
};
#[repr(transparent)]
pub struct RefCount<T>(pub(super) Arc<RwLock<T>>);
#[repr(transparent)]
pub struct Weak<T>(pub(super) std::sync::Weak<RwLock<T>>);
pub struct Ref<'a, T: ?Sized>(RwLockReadGuard<'a, T>);