Compare commits

...

2 commits

Author SHA1 Message Date
ad0d79acc1 Added FirstLoginConfig 2025-05-24 00:12:49 +02:00
aec5a50d3e README update for CNBeta2.0.4 2025-05-23 23:00:59 +02:00
8 changed files with 158 additions and 46 deletions

View file

@ -43,7 +43,7 @@ The configuration of each server is located under the `config` directory (create
- To change gameplay-related settings, edit one of game-server configuration files, for example: gacha banner schedule is located in: `config/40-game-server/gacha_schedule.toml`
### Logging in
Currently supported client version is `OSPRODWin1.7.0`, you can get it from 3rd party sources (e.g. game launcher). Next, you have to apply the necessary [client patch](https://git.xeondev.com/vivian-rs/vivian-patch). It allows you to connect to the local server and replaces encryption keys with custom ones.
Currently supported client version is `CNBetaWin2.0.4`, you can get it from 3rd party sources (e.g. torrent). Next, you have to apply the necessary [client patch](https://git.xeondev.com/traffic95/vivian-patch/src/branch/CNBeta2.0.4). It allows you to connect to the local server and replaces encryption keys with custom ones.
### Obtaining characters without gacha
While playing on the server, you may want to obtain the characters that are not available in current gacha schedule. In order to do so, you can use in-game debug menu: find the 'Vivian' NPC in the WorkShop location, and interact with it. It'll send you to a debug TV mode scene, where you can select the characters to obtain. (The NPC will spawn as soon as you progress in starting quests!)

View file

@ -0,0 +1,23 @@
interknot_level = 60
control_avatar_id = 2011
control_guise_avatar_id = 1371
day_of_week = 5
start_main_quest = false
default_section_id = 1
[avatar]
unlock_all = true
unlock_id_list = [ 1011, 1081 ]
level = 60
rank = 6
unlocked_talent_num = 0
talent_switch = [ false, false, false, false, false, false ]
passive_skill_level = 7
skill_level_map = [ 12, 12, 12, 12, 7, 7, 12 ]
[weapon]
unlock_all = true
unlock_id_list = [ 13101 ]
level = 60
star = 5
refine_level = 1

View file

@ -43,3 +43,38 @@ pub struct GachaMaterialConfig {
pub id: u32,
pub count: u32,
}
#[derive(Deserialize)]
pub struct FirstLoginConfig {
pub interknot_level: u32,
pub control_avatar_id: u32,
pub control_guise_avatar_id: u32,
pub day_of_week: u32,
pub start_main_quest: bool,
pub default_section_id: u32,
pub avatar: FirstLoginAvatarConfig,
pub weapon: FirstLoginWeaponConfig,
}
#[derive(Deserialize)]
pub struct FirstLoginAvatarConfig {
pub unlock_all: bool,
#[serde(default)]
pub unlock_id_list: Vec<u32>,
pub level: u32,
pub rank: u32,
pub unlocked_talent_num: u32,
pub talent_switch: Vec<bool>,
pub passive_skill_level: u32,
pub skill_level_map: Vec<u32>,
}
#[derive(Deserialize)]
pub struct FirstLoginWeaponConfig {
pub unlock_all: bool,
#[serde(default)]
pub unlock_id_list: Vec<u32>,
pub level: u32,
pub star: u32,
pub refine_level: u32,
}

View file

@ -10,11 +10,10 @@ use vivian_logic::{
use vivian_proto::server_only::{AvatarData, AvatarItemInfo};
use crate::{
logic::{
config::FirstLoginAvatarConfig, logic::{
property::{Property, PropertyHashMap},
sync::{LoginDataSyncComponent, PlayerSyncComponent, SyncType},
},
resources::NapResources,
}, resources::NapResources
};
use super::{Model, Saveable};
@ -28,16 +27,24 @@ pub struct AvatarModel {
impl AvatarModel {
pub fn on_first_login(&mut self, res: &NapResources) {
const STARTING_AVATARS: &[u32] = &[1011, 1081];
let cfg = &res.gameplay.first_login.avatar;
STARTING_AVATARS
.iter()
.filter_map(|id| {
res.templates
.avatar_base_template_tb()
.find(|tmpl| tmpl.id() == *id)
})
.for_each(|tmpl| self.unlock_avatar(&tmpl, None));
if cfg.unlock_all {
res
.templates
.avatar_base_template_tb()
.for_each(|tmpl| self.unlock_avatar(&tmpl, None, Some(cfg)));
} else {
cfg
.unlock_id_list
.iter()
.filter_map(|id| {
res.templates
.avatar_base_template_tb()
.find(|tmpl| tmpl.id() == *id)
})
.for_each(|tmpl| self.unlock_avatar(&tmpl, None, Some(cfg)));
}
}
pub fn send_add_avatar_notify(&self, listener: &mut dyn NotifyListener) {
@ -96,6 +103,7 @@ impl AvatarModel {
&mut self,
base_template: &AvatarBaseTemplate,
perform_type: Option<vivian_proto::add_avatar_sc_notify::PerformType>,
first_login_cfg: Option<&FirstLoginAvatarConfig>,
) {
const AVATAR_BLACKLIST: &[u32] = &[];
@ -105,20 +113,28 @@ impl AvatarModel {
&& !self.avatar_map.contains_key(&avatar_id)
&& !AVATAR_BLACKLIST.contains(&avatar_id)
{
let mut skill_level_map: HashMap<EAvatarSkillType, u32> = (0..EAvatarSkillType::EnumCount.into())
.map(|ty| EAvatarSkillType::try_from(ty).unwrap())
.zip([0].into_iter().cycle())
.collect();
if let Some(cfg) = first_login_cfg {
skill_level_map = (0..EAvatarSkillType::EnumCount.into())
.map(|ty| EAvatarSkillType::try_from(ty).unwrap())
.zip(cfg.skill_level_map.clone())
.collect();
}
self.avatar_map.insert(
avatar_id,
AvatarItem {
id: avatar_id,
level: 1,
level: first_login_cfg.map_or(1, |cfg| cfg.level),
exp: 0,
rank: 1,
unlocked_talent_num: 0,
talent_switch: [false; 6],
passive_skill_level: 0,
skill_level_map: (0..EAvatarSkillType::EnumCount.into())
.map(|ty| EAvatarSkillType::try_from(ty).unwrap())
.zip([0].into_iter().cycle())
.collect(),
rank: first_login_cfg.map_or(1, |cfg| cfg.rank),
unlocked_talent_num: first_login_cfg.map_or(0, |cfg| cfg.unlocked_talent_num),
talent_switch: first_login_cfg.map_or([false; 6], |cfg| cfg.talent_switch.as_slice().try_into().unwrap()),
passive_skill_level: first_login_cfg.map_or(0, |cfg| cfg.passive_skill_level),
skill_level_map,
weapon_uid: 0,
dressed_equip_map: HashMap::new(),
first_get_time: time_util::unix_timestamp_seconds(),

View file

@ -37,21 +37,47 @@ impl ItemModel {
.avatar_skin_base_template_tb()
.for_each(|tmpl| self.item_count_map.insert(tmpl.id(), 1));
// Unlock all weapons as well
res.templates.weapon_template_tb().for_each(|tmpl| {
let uid = self.next_uid();
self.weapon_map.insert(
uid,
WeaponItem {
id: tmpl.item_id(),
level: 60,
exp: 0,
star: tmpl.star_limit() + 1,
refine_level: tmpl.refine_limit(),
lock: false,
},
);
});
let weapon_cfg = &res.gameplay.first_login.weapon;
if weapon_cfg.unlock_all {
res.templates.weapon_template_tb().for_each(|tmpl| {
let uid = self.next_uid();
self.weapon_map.insert(
uid,
WeaponItem {
id: tmpl.item_id(),
level: weapon_cfg.level,
exp: 0,
star: weapon_cfg.star,
refine_level: weapon_cfg.refine_level,
lock: false,
},
);
});
} else {
weapon_cfg
.unlock_id_list
.iter()
.filter_map(|id| {
res.templates
.weapon_template_tb()
.find(|tmpl| tmpl.item_id() == *id)
})
.for_each(|tmpl| {
let uid = self.next_uid();
self.weapon_map.insert(
uid,
WeaponItem {
id: tmpl.item_id(),
level: weapon_cfg.level,
exp: 0,
star: weapon_cfg.star,
refine_level: weapon_cfg.refine_level,
lock: false,
},
);
});
}
}
pub fn add_weapon(&mut self, template: &WeaponTemplate) -> u32 {

View file

@ -140,21 +140,28 @@ impl Player {
// and it won't be needed to set avatar ids in basic module
// (player will select them in beginner procedure)
let cfg = &self.resources.gameplay.first_login;
self.avatar_model.on_first_login(self.resources);
self.basic_model.level.set(1);
self.basic_model.avatar_id.set(2011);
self.basic_model.control_avatar_id.set(2011);
self.basic_model.control_guise_avatar_id.set(2011);
self.basic_model.level.set(cfg.interknot_level);
self.basic_model.avatar_id.set(cfg.control_avatar_id);
self.basic_model.control_avatar_id.set(cfg.control_avatar_id);
self.basic_model.control_guise_avatar_id.set(cfg.control_guise_avatar_id);
self.main_city_model.day_of_week.set(5); // Friday
self.main_city_model.day_of_week.set(cfg.day_of_week);
self.item_model.on_first_login(self.resources);
self.misc_model.on_first_login(self.resources);
self.gacha_model.on_first_login();
let mut main_city_quest_id = 10020001;
if !cfg.start_main_quest {
main_city_quest_id = 10020028;
}
self.quest_model
.add_main_city_quest(10020001, self.resources);
.add_main_city_quest(main_city_quest_id, self.resources);
// Initialize hall scene with WorkShop section
let scene_uid = self.scene_model.next_scene_uid();
@ -163,7 +170,7 @@ impl Player {
scene::SceneSnapshot {
scene_id: 1,
ext: scene::SceneSnapshotExt::Hall(scene::HallSceneSnapshot {
cur_section_id: 2,
cur_section_id: cfg.default_section_id,
sections: HashMap::new(),
main_city_objects_state: HashMap::new(),
}),
@ -234,7 +241,7 @@ impl Player {
AddItemSource::Mail => Some(PerformType::PerformAnimation),
};
self.avatar_model.unlock_avatar(&template, perform_type);
self.avatar_model.unlock_avatar(&template, perform_type, None);
}
None
}

View file

@ -47,6 +47,10 @@ async fn main() -> Result<(), StartupError> {
concatcp!(CONFIG_DIR, "gacha_schedule.toml"),
include_str!("../gacha_schedule.default.toml"),
),
first_login: common::config_util::load_or_create(
concatcp!(CONFIG_DIR, "first_login.toml"),
include_str!("../first_login.default.toml"),
),
};
let resources = NapResources::load(&config.resources, gameplay_cfg)?;

View file

@ -5,7 +5,7 @@ use config::{
LoadConditionsError, TemplateCollection, TemplateCollectionError,
};
use crate::config::{GachaScheduleConfig, ResourceConfig};
use crate::config::{FirstLoginConfig, GachaScheduleConfig, ResourceConfig};
#[derive(thiserror::Error, Debug)]
pub enum LoadResourcesError {
@ -40,6 +40,7 @@ pub struct NapResources {
pub struct ServerGameplayConfig {
pub gacha_schedule: GachaScheduleConfig,
pub first_login: FirstLoginConfig,
}
impl NapResources {