mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Map selector and generation UI
This commit is contained in:
parent
b372a0c836
commit
f4ca60cbb6
@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Keybinds for zooming the camera (Defaults: ']' for zooming in and '[' for zooming out)
|
||||
- Added the ability to make pets sit, they wont follow nor defend you in this state
|
||||
- Portals that spawn in place of the last staircase at old style dungeons to prevent stair cheesing
|
||||
- Mutliple singleplayer worlds and map generation UI.
|
||||
|
||||
### Changed
|
||||
|
||||
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -7231,6 +7231,7 @@ dependencies = [
|
||||
"egui_wgpu_backend",
|
||||
"egui_winit_platform",
|
||||
"enum-iterator 1.4.1",
|
||||
"enum-map",
|
||||
"etagere",
|
||||
"euc",
|
||||
"gilrs",
|
||||
|
@ -32,6 +32,23 @@ main-login_process =
|
||||
You can create an account over at
|
||||
|
||||
https://veloren.net/account/.
|
||||
main-singleplayer-new = New
|
||||
main-singleplayer-delete = Delete
|
||||
main-singleplayer-regenerate = Regenerate
|
||||
main-singleplayer-create_custom = Create Custom
|
||||
main-singleplayer-invalid_name = Error: Invalid name
|
||||
main-singleplayer-seed = Seed
|
||||
main-singleplayer-random_seed = Random
|
||||
main-singleplayer-size_lg = Logarithmic size
|
||||
main-singleplayer-map_large_warning = Warning: Large worlds will take a long time to start for the first time
|
||||
main-singleplayer-world_name = World name
|
||||
main-singleplayer-map_scale = Vertical scaling
|
||||
main-singleplayer-map_erosion_quality = Erosion quality
|
||||
main-singleplayer-map_shape = Shape
|
||||
main-singleplayer-play = Play
|
||||
main-singleplayer-generate_and_play = Generate & Play
|
||||
menu-singleplayer-confirm_delete = Are you sure you want to delete "{ $world_name }"
|
||||
menu-singleplayer-confirm_regenerate = Are you sure you want to regenerate "{ $world_name }"
|
||||
main-login-server_not_found = Server not found
|
||||
main-login-authentication_error = Auth error on server
|
||||
main-login-internal_error = Internal error on client (most likely, player character was deleted)
|
||||
|
@ -75,6 +75,24 @@ impl PlayerPhysicsSetting {
|
||||
pub fn client_authoritative(&self) -> bool { !self.server_authoritative() }
|
||||
}
|
||||
|
||||
/// Describe how the map should be generated.
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, enum_map::Enum)]
|
||||
pub enum MapKind {
|
||||
/// The normal square map, with oceans beyond the map edge
|
||||
Square,
|
||||
/// A more circular map, might have more islands
|
||||
Circle,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MapKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MapKind::Square => f.write_str("Square"),
|
||||
MapKind::Circle => f.write_str("Circle"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of which players are using client-authoratative vs server-authoratative
|
||||
/// physics, as a stop-gap until we can use server-authoratative physics for
|
||||
/// everyone
|
||||
|
@ -55,7 +55,11 @@ impl Site {
|
||||
.get(*faction)
|
||||
.map_or(false, |f| f.good_or_evil == good_or_evil)
|
||||
})
|
||||
.min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos))
|
||||
.min_by_key(|(faction_wpos, _)| {
|
||||
faction_wpos
|
||||
.as_::<i64>()
|
||||
.distance_squared(wpos.as_::<i64>())
|
||||
})
|
||||
.map(|(_, faction)| *faction)
|
||||
}),
|
||||
population: Default::default(),
|
||||
|
@ -46,7 +46,7 @@ fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
|
||||
// Only include sites in the list if they're not the current one and they're more populus
|
||||
.filter(|(other_id, _, other_site2)| *other_id != site_id && other_site2.plots().len() > site2.plots().len())
|
||||
.collect::<Vec<_>>();
|
||||
other_sites.sort_by_key(|(_, other, _)| other.wpos.distance_squared(site.wpos) as i64);
|
||||
other_sites.sort_by_key(|(_, other, _)| other.wpos.as_::<i64>().distance_squared(site.wpos.as_::<i64>()));
|
||||
let mut max_size = 0;
|
||||
// Remove sites that aren't in increasing order of size (Stalin sort?!)
|
||||
other_sites.retain(|(_, _, other_site2)| {
|
||||
|
@ -131,8 +131,8 @@ use {
|
||||
use crate::persistence::character_loader::CharacterScreenResponseKind;
|
||||
use common::comp::Anchor;
|
||||
#[cfg(feature = "worldgen")]
|
||||
use world::{
|
||||
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
|
||||
pub use world::{
|
||||
sim::{FileOpts, GenOpts, WorldOpts, DEFAULT_WORLD_MAP},
|
||||
IndexOwned, World,
|
||||
};
|
||||
|
||||
|
@ -363,6 +363,7 @@ const MIGRATION_UPGRADE_GUARANTEE: &str = "Any valid file of an old verison shou
|
||||
successfully migrate to the latest version.";
|
||||
|
||||
/// Combines all the editable settings into one struct that is stored in the ecs
|
||||
#[derive(Clone)]
|
||||
pub struct EditableSettings {
|
||||
pub whitelist: Whitelist,
|
||||
pub banlist: Banlist,
|
||||
|
@ -134,6 +134,7 @@ itertools = { workspace = true }
|
||||
|
||||
# Discord RPC
|
||||
discord-sdk = { version = "0.3.0", optional = true }
|
||||
enum-map = "2.5.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
dispatch = "0.1.4"
|
||||
|
@ -42,6 +42,8 @@ pub mod window;
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
use crate::singleplayer::Singleplayer;
|
||||
#[cfg(feature = "singleplayer")]
|
||||
use crate::singleplayer::SingleplayerState;
|
||||
#[cfg(feature = "egui-ui")]
|
||||
use crate::ui::egui::EguiState;
|
||||
use crate::{
|
||||
@ -74,7 +76,7 @@ pub struct GlobalState {
|
||||
pub info_message: Option<String>,
|
||||
pub clock: Clock,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
pub singleplayer: Option<Singleplayer>,
|
||||
pub singleplayer: SingleplayerState,
|
||||
// TODO: redo this so that the watcher doesn't have to exist for reloading to occur
|
||||
pub i18n: LocalizationHandle,
|
||||
pub clipboard: iced_winit::Clipboard,
|
||||
@ -102,7 +104,7 @@ impl GlobalState {
|
||||
#[cfg(feature = "singleplayer")]
|
||||
pub fn paused(&self) -> bool {
|
||||
self.singleplayer
|
||||
.as_ref()
|
||||
.as_running()
|
||||
.map_or(false, Singleplayer::is_paused)
|
||||
}
|
||||
|
||||
@ -110,10 +112,10 @@ impl GlobalState {
|
||||
pub fn paused(&self) -> bool { false }
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
pub fn unpause(&self) { self.singleplayer.as_ref().map(|s| s.pause(false)); }
|
||||
pub fn unpause(&self) { self.singleplayer.as_running().map(|s| s.pause(false)); }
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
pub fn pause(&self) { self.singleplayer.as_ref().map(|s| s.pause(true)); }
|
||||
pub fn pause(&self) { self.singleplayer.as_running().map(|s| s.pause(true)); }
|
||||
}
|
||||
|
||||
// TODO: appears to be currently unused by playstates
|
||||
|
@ -19,6 +19,8 @@ static GLOBAL: common_base::tracy_client::ProfiledAllocator<std::alloc::System>
|
||||
common_base::tracy_client::ProfiledAllocator::new(std::alloc::System, 128);
|
||||
|
||||
use i18n::{self, LocalizationHandle};
|
||||
#[cfg(feature = "singleplayer")]
|
||||
use veloren_voxygen::singleplayer::SingleplayerState;
|
||||
use veloren_voxygen::{
|
||||
audio::AudioFrontend,
|
||||
panic_handler,
|
||||
@ -223,7 +225,7 @@ fn main() {
|
||||
settings,
|
||||
info_message: None,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
singleplayer: None,
|
||||
singleplayer: SingleplayerState::None,
|
||||
i18n,
|
||||
clipboard,
|
||||
clear_shadows_next_frame: false,
|
||||
|
@ -1325,36 +1325,27 @@ impl Controls {
|
||||
];
|
||||
|
||||
let right_column_content = if character_id.is_none() {
|
||||
let site_slider = starter_slider(
|
||||
i18n.get_msg("char_selection-starting_site").into_owned(),
|
||||
30,
|
||||
&mut sliders.starting_site,
|
||||
self.possible_starting_sites.len() as u32 - 1,
|
||||
*start_site_idx as u32,
|
||||
|x| Message::StartingSite(x as usize),
|
||||
imgs,
|
||||
);
|
||||
let map_sz = Vec2::new(500, 500);
|
||||
let map_img = Image::new(self.map_img)
|
||||
.height(Length::Units(map_sz.x))
|
||||
.width(Length::Units(map_sz.y));
|
||||
let site_name = Text::new(
|
||||
self.possible_starting_sites[*start_site_idx]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown")
|
||||
)
|
||||
.horizontal_alignment(HorizontalAlignment::Left)
|
||||
.color(Color::from_rgb(131.0, 102.0, 0.0))
|
||||
/* .stroke(Stroke {
|
||||
color: Color::WHITE,
|
||||
width: 1.0,
|
||||
}) */;
|
||||
}) */
|
||||
//TODO: Add text-outline here whenever we updated iced to a version supporting
|
||||
// this
|
||||
|
||||
let map = if let Some(info) = self.possible_starting_sites.get(*start_site_idx)
|
||||
{
|
||||
let site_name = Text::new(
|
||||
self.possible_starting_sites[*start_site_idx]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown"),
|
||||
)
|
||||
.horizontal_alignment(HorizontalAlignment::Left)
|
||||
.color(Color::from_rgb(131.0, 102.0, 0.0));
|
||||
let pos_frac = info
|
||||
.wpos
|
||||
.map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
|
||||
@ -1388,6 +1379,15 @@ impl Controls {
|
||||
if self.possible_starting_sites.is_empty() {
|
||||
vec![map]
|
||||
} else {
|
||||
let site_slider = starter_slider(
|
||||
i18n.get_msg("char_selection-starting_site").into_owned(),
|
||||
30,
|
||||
&mut sliders.starting_site,
|
||||
self.possible_starting_sites.len() as u32 - 1,
|
||||
*start_site_idx as u32,
|
||||
|x| Message::StartingSite(x as usize),
|
||||
imgs,
|
||||
);
|
||||
let site_buttons = Row::with_children(vec![
|
||||
neat_button(
|
||||
prev_starting_site_button,
|
||||
@ -1972,9 +1972,9 @@ impl CharSelectionUi {
|
||||
let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
let default_name = match global_state.singleplayer {
|
||||
Some(_) => String::new(),
|
||||
None => global_state.settings.networking.username.clone(),
|
||||
let default_name = match global_state.singleplayer.is_running() {
|
||||
true => String::new(),
|
||||
false => global_state.settings.networking.username.clone(),
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "singleplayer"))]
|
||||
|
@ -4,7 +4,7 @@ mod ui;
|
||||
|
||||
use super::char_selection::CharSelectionState;
|
||||
#[cfg(feature = "singleplayer")]
|
||||
use crate::singleplayer::Singleplayer;
|
||||
use crate::singleplayer::SingleplayerState;
|
||||
use crate::{
|
||||
render::{Drawer, GlobalsBindGroup},
|
||||
settings::Settings,
|
||||
@ -73,7 +73,7 @@ impl PlayState for MainMenuState {
|
||||
// Reset singleplayer server if it was running already
|
||||
#[cfg(feature = "singleplayer")]
|
||||
{
|
||||
global_state.singleplayer = None;
|
||||
global_state.singleplayer = SingleplayerState::None;
|
||||
}
|
||||
|
||||
// Updated localization in case the selected language was changed
|
||||
@ -97,7 +97,7 @@ impl PlayState for MainMenuState {
|
||||
// Poll server creation
|
||||
#[cfg(feature = "singleplayer")]
|
||||
{
|
||||
if let Some(singleplayer) = &global_state.singleplayer {
|
||||
if let Some(singleplayer) = global_state.singleplayer.as_running() {
|
||||
match singleplayer.receiver.try_recv() {
|
||||
Ok(Ok(())) => {
|
||||
// Attempt login after the server is finished initializing
|
||||
@ -113,7 +113,7 @@ impl PlayState for MainMenuState {
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
error!(?e, "Could not start server");
|
||||
global_state.singleplayer = None;
|
||||
global_state.singleplayer = SingleplayerState::None;
|
||||
self.init = InitState::None;
|
||||
self.main_menu_ui.cancel_connection();
|
||||
let server_err = match e {
|
||||
@ -332,7 +332,7 @@ impl PlayState for MainMenuState {
|
||||
// blocking TODO fix when the network rework happens
|
||||
#[cfg(feature = "singleplayer")]
|
||||
{
|
||||
global_state.singleplayer = None;
|
||||
global_state.singleplayer = SingleplayerState::None;
|
||||
}
|
||||
self.init = InitState::None;
|
||||
self.main_menu_ui.cancel_connection();
|
||||
@ -351,9 +351,32 @@ impl PlayState for MainMenuState {
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
MainMenuEvent::StartSingleplayer => {
|
||||
let singleplayer = Singleplayer::new(&global_state.tokio_runtime);
|
||||
|
||||
global_state.singleplayer = Some(singleplayer);
|
||||
global_state.singleplayer.run(&global_state.tokio_runtime);
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
MainMenuEvent::InitSingleplayer => {
|
||||
global_state.singleplayer = SingleplayerState::init();
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
MainMenuEvent::SinglePlayerChange(change) => {
|
||||
if let SingleplayerState::Init(ref mut init) = global_state.singleplayer {
|
||||
match change {
|
||||
ui::WorldsChange::SetActive(world) => init.current = world,
|
||||
ui::WorldsChange::Delete(world) => init.remove(world),
|
||||
ui::WorldsChange::Regenerate(world) => init.delete_map_file(world),
|
||||
ui::WorldsChange::AddNew => init.new_world(),
|
||||
ui::WorldsChange::CurrentWorldChange(change) => {
|
||||
if let Some(world) = init
|
||||
.current
|
||||
.map(|i| &mut init.worlds[i])
|
||||
.filter(|map| !map.is_generated)
|
||||
{
|
||||
change.apply(world);
|
||||
init.save_current_meta();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
MainMenuEvent::Quit => return PlayStateResult::Shutdown,
|
||||
// Note: Keeping in case we re-add the disclaimer
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::{Imgs, LoginInfo, Message, FILL_FRAC_ONE, FILL_FRAC_TWO};
|
||||
use super::{Imgs, LoginInfo, Message, Showing, FILL_FRAC_ONE, FILL_FRAC_TWO};
|
||||
use crate::ui::{
|
||||
fonts::IcedFonts as Fonts,
|
||||
ice::{
|
||||
@ -11,6 +11,7 @@ use crate::ui::{
|
||||
Element,
|
||||
},
|
||||
};
|
||||
|
||||
use i18n::{LanguageMetadata, Localization};
|
||||
use iced::{
|
||||
button, scrollable, text_input, Align, Button, Column, Container, Length, Row, Scrollable,
|
||||
@ -22,6 +23,7 @@ const INPUT_WIDTH: u16 = 230;
|
||||
const INPUT_TEXT_SIZE: u16 = 20;
|
||||
|
||||
/// Login screen for the main menu
|
||||
#[derive(Default)]
|
||||
pub struct Screen {
|
||||
quit_button: button::State,
|
||||
// settings_button: button::State,
|
||||
@ -36,21 +38,6 @@ pub struct Screen {
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
servers_button: Default::default(),
|
||||
credits_button: Default::default(),
|
||||
// settings_button: Default::default(),
|
||||
quit_button: Default::default(),
|
||||
language_select_button: Default::default(),
|
||||
|
||||
error_okay_button: Default::default(),
|
||||
|
||||
banner: LoginBanner::new(),
|
||||
language_selection: LanguageSelectBanner::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn view(
|
||||
&mut self,
|
||||
fonts: &Fonts,
|
||||
@ -59,7 +46,7 @@ impl Screen {
|
||||
login_info: &LoginInfo,
|
||||
error: Option<&str>,
|
||||
i18n: &Localization,
|
||||
is_selecting_language: bool,
|
||||
show: &Showing,
|
||||
selected_language_index: Option<usize>,
|
||||
language_metadatas: &[LanguageMetadata],
|
||||
button_style: style::button::Style,
|
||||
@ -171,24 +158,25 @@ impl Screen {
|
||||
.height(Length::Units(180))
|
||||
.padding(20)
|
||||
.into()
|
||||
} else if is_selecting_language {
|
||||
self.language_selection.view(
|
||||
fonts,
|
||||
imgs,
|
||||
i18n,
|
||||
language_metadatas,
|
||||
selected_language_index,
|
||||
button_style,
|
||||
)
|
||||
} else {
|
||||
self.banner.view(
|
||||
fonts,
|
||||
imgs,
|
||||
server_field_locked,
|
||||
login_info,
|
||||
i18n,
|
||||
button_style,
|
||||
)
|
||||
match show {
|
||||
Showing::Login => self.banner.view(
|
||||
fonts,
|
||||
imgs,
|
||||
server_field_locked,
|
||||
login_info,
|
||||
i18n,
|
||||
button_style,
|
||||
),
|
||||
Showing::Languages => self.language_selection.view(
|
||||
fonts,
|
||||
imgs,
|
||||
i18n,
|
||||
language_metadatas,
|
||||
selected_language_index,
|
||||
button_style,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
let central_column = Container::new(central_content)
|
||||
@ -222,6 +210,7 @@ impl Screen {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LanguageSelectBanner {
|
||||
okay_button: button::State,
|
||||
language_buttons: Vec<button::State>,
|
||||
@ -230,14 +219,6 @@ pub struct LanguageSelectBanner {
|
||||
}
|
||||
|
||||
impl LanguageSelectBanner {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
okay_button: Default::default(),
|
||||
language_buttons: Default::default(),
|
||||
selection_list: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(
|
||||
&mut self,
|
||||
fonts: &Fonts,
|
||||
@ -336,6 +317,7 @@ impl LanguageSelectBanner {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LoginBanner {
|
||||
pub username: text_input::State,
|
||||
pub password: text_input::State,
|
||||
@ -349,20 +331,6 @@ pub struct LoginBanner {
|
||||
}
|
||||
|
||||
impl LoginBanner {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
username: Default::default(),
|
||||
password: Default::default(),
|
||||
server: Default::default(),
|
||||
|
||||
multiplayer_button: Default::default(),
|
||||
#[cfg(feature = "singleplayer")]
|
||||
singleplayer_button: Default::default(),
|
||||
|
||||
unlock_server_field_button: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(
|
||||
&mut self,
|
||||
fonts: &Fonts,
|
||||
|
@ -4,6 +4,8 @@ mod connecting;
|
||||
mod credits;
|
||||
mod login;
|
||||
mod servers;
|
||||
#[cfg(feature = "singleplayer")]
|
||||
mod world_selector;
|
||||
|
||||
use crate::{
|
||||
credits::Credits,
|
||||
@ -53,6 +55,12 @@ image_ids_ice! {
|
||||
selection: "voxygen.element.ui.generic.frames.selection",
|
||||
selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
|
||||
selection_press: "voxygen.element.ui.generic.frames.selection_press",
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
slider_range: "voxygen.element.ui.generic.slider.track",
|
||||
#[cfg(feature = "singleplayer")]
|
||||
slider_indicator: "voxygen.element.ui.generic.slider.indicator",
|
||||
|
||||
unlock: "voxygen.element.ui.generic.buttons.unlock",
|
||||
unlock_hover: "voxygen.element.ui.generic.buttons.unlock_hover",
|
||||
unlock_press: "voxygen.element.ui.generic.buttons.unlock_press",
|
||||
@ -77,6 +85,47 @@ const BG_IMGS: [&str; 14] = [
|
||||
"voxygen.background.bg_14",
|
||||
];
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
#[derive(Clone)]
|
||||
pub enum WorldChange {
|
||||
Name(String),
|
||||
Seed(u32),
|
||||
SizeX(u32),
|
||||
SizeY(u32),
|
||||
Scale(f64),
|
||||
MapKind(common::resources::MapKind),
|
||||
ErosionQuality(f32),
|
||||
DefaultGenOps,
|
||||
}
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
impl WorldChange {
|
||||
pub fn apply(self, world: &mut crate::singleplayer::SingleplayerWorld) {
|
||||
let mut def = Default::default();
|
||||
let gen_opts = world.gen_opts.as_mut().unwrap_or(&mut def);
|
||||
match self {
|
||||
WorldChange::Name(name) => world.name = name,
|
||||
WorldChange::Seed(seed) => world.seed = seed,
|
||||
WorldChange::SizeX(s) => gen_opts.x_lg = s,
|
||||
WorldChange::SizeY(s) => gen_opts.y_lg = s,
|
||||
WorldChange::Scale(scale) => gen_opts.scale = scale,
|
||||
WorldChange::MapKind(kind) => gen_opts.map_kind = kind,
|
||||
WorldChange::ErosionQuality(q) => gen_opts.erosion_quality = q,
|
||||
WorldChange::DefaultGenOps => world.gen_opts = Some(Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
#[derive(Clone)]
|
||||
pub enum WorldsChange {
|
||||
SetActive(Option<usize>),
|
||||
Delete(usize),
|
||||
Regenerate(usize),
|
||||
AddNew,
|
||||
CurrentWorldChange(WorldChange),
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
LoginAttempt {
|
||||
username: String,
|
||||
@ -87,6 +136,10 @@ pub enum Event {
|
||||
ChangeLanguage(LanguageMetadata),
|
||||
#[cfg(feature = "singleplayer")]
|
||||
StartSingleplayer,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
InitSingleplayer,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
SinglePlayerChange(WorldsChange),
|
||||
Quit,
|
||||
// Note: Keeping in case we re-add the disclaimer
|
||||
//DisclaimerAccepted,
|
||||
@ -127,6 +180,26 @@ enum Screen {
|
||||
screen: connecting::Screen,
|
||||
connection_state: ConnectionState,
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
WorldSelector {
|
||||
screen: world_selector::Screen,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum Showing {
|
||||
Login,
|
||||
Languages,
|
||||
}
|
||||
|
||||
impl Showing {
|
||||
fn toggle(&mut self, other: Showing) {
|
||||
if *self == other {
|
||||
*self = Showing::Login;
|
||||
} else {
|
||||
*self = other;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Controls {
|
||||
@ -147,7 +220,7 @@ struct Controls {
|
||||
selected_server_index: Option<usize>,
|
||||
login_info: LoginInfo,
|
||||
|
||||
is_selecting_language: bool,
|
||||
show: Showing,
|
||||
selected_language_index: Option<usize>,
|
||||
|
||||
time: f64,
|
||||
@ -163,6 +236,14 @@ enum Message {
|
||||
ShowCredits,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Singleplayer,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
SingleplayerPlay,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
WorldChanged(WorldsChange),
|
||||
#[cfg(feature = "singleplayer")]
|
||||
WorldCancelConfirmation,
|
||||
#[cfg(feature = "singleplayer")]
|
||||
WorldConfirmation(world_selector::Confirmation),
|
||||
Multiplayer,
|
||||
UnlockServerField,
|
||||
LanguageChanged(usize),
|
||||
@ -202,7 +283,7 @@ impl Controls {
|
||||
}
|
||||
} else { */
|
||||
Screen::Login {
|
||||
screen: Box::new(login::Screen::new()),
|
||||
screen: Box::default(),
|
||||
error: None,
|
||||
};
|
||||
//};
|
||||
@ -237,7 +318,7 @@ impl Controls {
|
||||
selected_server_index,
|
||||
login_info,
|
||||
|
||||
is_selecting_language: false,
|
||||
show: Showing::Login,
|
||||
selected_language_index,
|
||||
|
||||
time: 0.0,
|
||||
@ -251,6 +332,7 @@ impl Controls {
|
||||
settings: &Settings,
|
||||
key_layout: &Option<KeyLayout>,
|
||||
dt: f32,
|
||||
#[cfg(feature = "singleplayer")] worlds: &crate::singleplayer::SingleplayerWorlds,
|
||||
) -> Element<Message> {
|
||||
self.time += dt as f64;
|
||||
|
||||
@ -306,7 +388,7 @@ impl Controls {
|
||||
&self.login_info,
|
||||
error.as_deref(),
|
||||
&self.i18n.read(),
|
||||
self.is_selecting_language,
|
||||
&self.show,
|
||||
self.selected_language_index,
|
||||
&language_metadatas,
|
||||
button_style,
|
||||
@ -334,6 +416,14 @@ impl Controls {
|
||||
&settings.controls,
|
||||
key_layout,
|
||||
),
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Screen::WorldSelector { screen } => screen.view(
|
||||
&self.fonts,
|
||||
&self.imgs,
|
||||
worlds,
|
||||
&self.i18n.read(),
|
||||
button_style,
|
||||
),
|
||||
};
|
||||
|
||||
Container::new(
|
||||
@ -360,7 +450,7 @@ impl Controls {
|
||||
Message::Quit => events.push(Event::Quit),
|
||||
Message::Back => {
|
||||
self.screen = Screen::Login {
|
||||
screen: Box::new(login::Screen::new()),
|
||||
screen: Box::default(),
|
||||
error: None,
|
||||
};
|
||||
},
|
||||
@ -380,12 +470,52 @@ impl Controls {
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Message::Singleplayer => {
|
||||
self.screen = Screen::WorldSelector {
|
||||
screen: world_selector::Screen::default(),
|
||||
};
|
||||
events.push(Event::InitSingleplayer);
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Message::SingleplayerPlay => {
|
||||
self.screen = Screen::Connecting {
|
||||
screen: connecting::Screen::new(ui),
|
||||
connection_state: ConnectionState::InProgress,
|
||||
};
|
||||
events.push(Event::StartSingleplayer);
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Message::WorldChanged(change) => {
|
||||
match change {
|
||||
WorldsChange::Delete(_) | WorldsChange::Regenerate(_) => {
|
||||
if let Screen::WorldSelector {
|
||||
screen: world_selector::Screen { confirmation, .. },
|
||||
} = &mut self.screen
|
||||
{
|
||||
*confirmation = None;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
events.push(Event::SinglePlayerChange(change))
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Message::WorldCancelConfirmation => {
|
||||
if let Screen::WorldSelector {
|
||||
screen: world_selector::Screen { confirmation, .. },
|
||||
} = &mut self.screen
|
||||
{
|
||||
*confirmation = None;
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "singleplayer")]
|
||||
Message::WorldConfirmation(new_confirmation) => {
|
||||
if let Screen::WorldSelector {
|
||||
screen: world_selector::Screen { confirmation, .. },
|
||||
} = &mut self.screen
|
||||
{
|
||||
*confirmation = Some(new_confirmation);
|
||||
}
|
||||
},
|
||||
Message::Multiplayer => {
|
||||
self.screen = Screen::Connecting {
|
||||
screen: connecting::Screen::new(ui),
|
||||
@ -403,7 +533,7 @@ impl Controls {
|
||||
Message::LanguageChanged(new_value) => {
|
||||
events.push(Event::ChangeLanguage(language_metadatas.remove(new_value)));
|
||||
},
|
||||
Message::OpenLanguageMenu => self.is_selecting_language = !self.is_selecting_language,
|
||||
Message::OpenLanguageMenu => self.show.toggle(Showing::Languages),
|
||||
Message::Password(new_value) => self.login_info.password = new_value,
|
||||
Message::Server(new_value) => {
|
||||
self.login_info.server = new_value;
|
||||
@ -451,7 +581,7 @@ impl Controls {
|
||||
if let Screen::Disclaimer { .. } = &self.screen {
|
||||
events.push(Event::DisclaimerAccepted);
|
||||
self.screen = Screen::Login {
|
||||
screen: login::Screen::new(),
|
||||
screen: login::Screen::default(),
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
@ -463,7 +593,7 @@ impl Controls {
|
||||
fn exit_connect_screen(&mut self) {
|
||||
if matches!(&self.screen, Screen::Connecting { .. }) {
|
||||
self.screen = Screen::Login {
|
||||
screen: Box::new(login::Screen::new()),
|
||||
screen: Box::default(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
@ -491,7 +621,7 @@ impl Controls {
|
||||
|| matches!(&self.screen, Screen::Login { .. })
|
||||
{
|
||||
self.screen = Screen::Login {
|
||||
screen: Box::new(login::Screen::new()),
|
||||
screen: Box::default(),
|
||||
error: Some(error),
|
||||
}
|
||||
} else {
|
||||
@ -626,11 +756,21 @@ impl MainMenuUi {
|
||||
pub fn maintain(&mut self, global_state: &mut GlobalState, dt: Duration) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
let worlds_default = crate::singleplayer::SingleplayerWorlds::default();
|
||||
#[cfg(feature = "singleplayer")]
|
||||
let worlds = global_state
|
||||
.singleplayer
|
||||
.as_init()
|
||||
.unwrap_or(&worlds_default);
|
||||
|
||||
let (messages, _) = self.ui.maintain(
|
||||
self.controls.view(
|
||||
&global_state.settings,
|
||||
&global_state.window.key_layout,
|
||||
dt.as_secs_f32(),
|
||||
#[cfg(feature = "singleplayer")]
|
||||
worlds,
|
||||
),
|
||||
global_state.window.renderer_mut(),
|
||||
None,
|
||||
|
630
voxygen/src/menu/main/ui/world_selector.rs
Normal file
630
voxygen/src/menu/main/ui/world_selector.rs
Normal file
@ -0,0 +1,630 @@
|
||||
use common::resources::MapKind;
|
||||
use i18n::Localization;
|
||||
use iced::{
|
||||
button, scrollable, slider, text_input, Align, Button, Column, Container, Length, Row,
|
||||
Scrollable, Slider, Space, Text, TextInput,
|
||||
};
|
||||
use rand::Rng;
|
||||
use vek::Rgba;
|
||||
|
||||
use crate::{
|
||||
menu::main::ui::{WorldsChange, FILL_FRAC_TWO},
|
||||
ui::{
|
||||
fonts::IcedFonts,
|
||||
ice::{
|
||||
component::neat_button,
|
||||
style,
|
||||
widget::{
|
||||
compound_graphic::{CompoundGraphic, Graphic},
|
||||
BackgroundContainer, Image, Overlay, Padding,
|
||||
},
|
||||
Element,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{Imgs, Message};
|
||||
|
||||
const INPUT_TEXT_SIZE: u16 = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Confirmation {
|
||||
Regenerate(usize),
|
||||
Delete(usize),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Screen {
|
||||
back_button: button::State,
|
||||
play_button: button::State,
|
||||
new_button: button::State,
|
||||
yes_button: button::State,
|
||||
no_button: button::State,
|
||||
|
||||
worlds_buttons: Vec<button::State>,
|
||||
|
||||
selection_list: scrollable::State,
|
||||
|
||||
world_name: text_input::State,
|
||||
map_seed: text_input::State,
|
||||
random_seed_button: button::State,
|
||||
world_size_x: slider::State,
|
||||
world_size_y: slider::State,
|
||||
|
||||
map_vertical_scale: slider::State,
|
||||
shape_buttons: enum_map::EnumMap<MapKind, button::State>,
|
||||
map_erosion_quality: slider::State,
|
||||
|
||||
delete_world: button::State,
|
||||
regenerate_map: button::State,
|
||||
generate_map: button::State,
|
||||
|
||||
pub confirmation: Option<Confirmation>,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub(super) fn view(
|
||||
&mut self,
|
||||
fonts: &IcedFonts,
|
||||
imgs: &Imgs,
|
||||
worlds: &crate::singleplayer::SingleplayerWorlds,
|
||||
i18n: &Localization,
|
||||
button_style: style::button::Style,
|
||||
) -> Element<Message> {
|
||||
let input_text_size = fonts.cyri.scale(INPUT_TEXT_SIZE);
|
||||
|
||||
let worlds_count = worlds.worlds.len();
|
||||
if self.worlds_buttons.len() != worlds_count {
|
||||
self.worlds_buttons = vec![Default::default(); worlds_count];
|
||||
}
|
||||
|
||||
let title = Text::new(i18n.get_msg("gameinput-map"))
|
||||
.size(fonts.cyri.scale(35))
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center);
|
||||
|
||||
let mut list = Scrollable::new(&mut self.selection_list)
|
||||
.spacing(8)
|
||||
.height(Length::Fill)
|
||||
.align_items(Align::Start);
|
||||
|
||||
let list_items = self
|
||||
.worlds_buttons
|
||||
.iter_mut()
|
||||
.zip(
|
||||
worlds
|
||||
.worlds
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, w)| (Some(i), &w.name)),
|
||||
)
|
||||
.map(|(state, (i, map))| {
|
||||
let color = if i == worlds.current {
|
||||
(97, 255, 18)
|
||||
} else {
|
||||
(97, 97, 25)
|
||||
};
|
||||
let button = Button::new(
|
||||
state,
|
||||
Row::with_children(vec![
|
||||
Space::new(Length::FillPortion(5), Length::Units(0)).into(),
|
||||
Text::new(map)
|
||||
.width(Length::FillPortion(95))
|
||||
.size(fonts.cyri.scale(25))
|
||||
.vertical_alignment(iced::VerticalAlignment::Center)
|
||||
.into(),
|
||||
]),
|
||||
)
|
||||
.style(
|
||||
style::button::Style::new(imgs.selection)
|
||||
.hover_image(imgs.selection_hover)
|
||||
.press_image(imgs.selection_press)
|
||||
.image_color(Rgba::new(color.0, color.1, color.2, 192)),
|
||||
)
|
||||
.min_height(56)
|
||||
.on_press(Message::WorldChanged(super::WorldsChange::SetActive(i)));
|
||||
Row::with_children(vec![
|
||||
Space::new(Length::FillPortion(3), Length::Units(0)).into(),
|
||||
button.width(Length::FillPortion(92)).into(),
|
||||
Space::new(Length::FillPortion(5), Length::Units(0)).into(),
|
||||
])
|
||||
});
|
||||
|
||||
for item in list_items {
|
||||
list = list.push(item);
|
||||
}
|
||||
|
||||
let new_button = Container::new(neat_button(
|
||||
&mut self.new_button,
|
||||
i18n.get_msg("main-singleplayer-new"),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(Message::WorldChanged(super::WorldsChange::AddNew)),
|
||||
))
|
||||
.center_x()
|
||||
.max_width(200);
|
||||
|
||||
let back_button = Container::new(neat_button(
|
||||
&mut self.back_button,
|
||||
i18n.get_msg("common-back"),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(Message::Back),
|
||||
))
|
||||
.center_x()
|
||||
.max_width(200);
|
||||
|
||||
let content = Column::with_children(vec![
|
||||
title.into(),
|
||||
list.into(),
|
||||
new_button.into(),
|
||||
back_button.into(),
|
||||
])
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.height(Length::FillPortion(38))
|
||||
.align_items(Align::Center)
|
||||
.padding(iced::Padding {
|
||||
bottom: 25,
|
||||
..iced::Padding::new(0)
|
||||
});
|
||||
|
||||
let selection_menu = BackgroundContainer::new(
|
||||
CompoundGraphic::from_graphics(vec![
|
||||
Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
|
||||
Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
|
||||
// TODO: use non image gradient
|
||||
Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
|
||||
])
|
||||
.fix_aspect_ratio()
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill),
|
||||
content,
|
||||
)
|
||||
.padding(Padding::new().horizontal(5).top(15));
|
||||
let mut items = vec![selection_menu.into()];
|
||||
|
||||
if let Some(i) = worlds.current {
|
||||
let world = &worlds.worlds[i];
|
||||
let can_edit = !world.is_generated;
|
||||
let message = |m| Message::WorldChanged(super::WorldsChange::CurrentWorldChange(m));
|
||||
|
||||
use super::WorldChange;
|
||||
|
||||
const SLIDER_TEXT_SIZE: u16 = 20;
|
||||
const SLIDER_CURSOR_SIZE: (u16, u16) = (9, 21);
|
||||
const SLIDER_BAR_HEIGHT: u16 = 9;
|
||||
const SLIDER_BAR_PAD: u16 = 0;
|
||||
// Height of interactable area
|
||||
const SLIDER_HEIGHT: u16 = 30;
|
||||
|
||||
let mut gen_content = vec![
|
||||
BackgroundContainer::new(
|
||||
Image::new(imgs.input_bg)
|
||||
.width(Length::Units(230))
|
||||
.fix_aspect_ratio(),
|
||||
if can_edit {
|
||||
Element::from(
|
||||
TextInput::new(
|
||||
&mut self.world_name,
|
||||
&i18n.get_msg("main-singleplayer-world_name"),
|
||||
&world.name,
|
||||
move |s| message(WorldChange::Name(s)),
|
||||
)
|
||||
.size(input_text_size),
|
||||
)
|
||||
} else {
|
||||
Text::new(&world.name)
|
||||
.size(input_text_size)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.into()
|
||||
},
|
||||
)
|
||||
.padding(Padding::new().horizontal(7).top(5))
|
||||
.into(),
|
||||
];
|
||||
|
||||
let seed = world.seed;
|
||||
let seed_str = i18n.get_msg("main-singleplayer-seed");
|
||||
let mut seed_content = vec![
|
||||
Column::with_children(vec![
|
||||
Text::new(seed_str.to_string())
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
])
|
||||
.padding(iced::Padding::new(5))
|
||||
.into(),
|
||||
BackgroundContainer::new(
|
||||
Image::new(imgs.input_bg)
|
||||
.width(Length::Units(190))
|
||||
.fix_aspect_ratio(),
|
||||
if can_edit {
|
||||
Element::from(
|
||||
TextInput::new(
|
||||
&mut self.map_seed,
|
||||
&seed_str,
|
||||
&seed.to_string(),
|
||||
move |s| {
|
||||
if let Ok(seed) = if s.is_empty() {
|
||||
Ok(0)
|
||||
} else {
|
||||
s.parse::<u32>()
|
||||
} {
|
||||
message(WorldChange::Seed(seed))
|
||||
} else {
|
||||
message(WorldChange::Seed(seed))
|
||||
}
|
||||
},
|
||||
)
|
||||
.size(input_text_size),
|
||||
)
|
||||
} else {
|
||||
Text::new(world.seed.to_string())
|
||||
.size(input_text_size)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Shrink)
|
||||
.into()
|
||||
},
|
||||
)
|
||||
.padding(Padding::new().horizontal(7).top(5))
|
||||
.into(),
|
||||
];
|
||||
|
||||
if can_edit {
|
||||
seed_content.push(
|
||||
Container::new(neat_button(
|
||||
&mut self.random_seed_button,
|
||||
i18n.get_msg("main-singleplayer-random_seed"),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(message(WorldChange::Seed(rand::thread_rng().gen()))),
|
||||
))
|
||||
.max_width(200)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
gen_content.push(Row::with_children(seed_content).into());
|
||||
|
||||
if let Some(gen_opts) = world.gen_opts.as_ref() {
|
||||
gen_content.push(
|
||||
Text::new(format!(
|
||||
"{}: x: {}, y: {}",
|
||||
i18n.get_msg("main-singleplayer-size_lg"),
|
||||
gen_opts.x_lg,
|
||||
gen_opts.y_lg
|
||||
))
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
);
|
||||
|
||||
if can_edit {
|
||||
gen_content.push(
|
||||
Row::with_children(vec![
|
||||
Slider::new(&mut self.world_size_x, 4..=13, gen_opts.x_lg, move |s| {
|
||||
message(WorldChange::SizeX(s))
|
||||
})
|
||||
.height(SLIDER_HEIGHT)
|
||||
.style(style::slider::Style::images(
|
||||
imgs.slider_indicator,
|
||||
imgs.slider_range,
|
||||
SLIDER_BAR_PAD,
|
||||
SLIDER_CURSOR_SIZE,
|
||||
SLIDER_BAR_HEIGHT,
|
||||
))
|
||||
.into(),
|
||||
Slider::new(&mut self.world_size_y, 4..=13, gen_opts.y_lg, move |s| {
|
||||
message(WorldChange::SizeY(s))
|
||||
})
|
||||
.height(SLIDER_HEIGHT)
|
||||
.style(style::slider::Style::images(
|
||||
imgs.slider_indicator,
|
||||
imgs.slider_range,
|
||||
SLIDER_BAR_PAD,
|
||||
SLIDER_CURSOR_SIZE,
|
||||
SLIDER_BAR_HEIGHT,
|
||||
))
|
||||
.into(),
|
||||
])
|
||||
.into(),
|
||||
);
|
||||
let height = Length::Units(56);
|
||||
if gen_opts.x_lg + gen_opts.y_lg >= 19 {
|
||||
gen_content.push(
|
||||
Text::new(i18n.get_msg("main-singleplayer-map_large_warning"))
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.height(height)
|
||||
.color([0.914, 0.835, 0.008])
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
gen_content.push(Space::new(Length::Units(0), height).into());
|
||||
}
|
||||
}
|
||||
|
||||
gen_content.push(
|
||||
Text::new(format!(
|
||||
"{}: {}",
|
||||
i18n.get_msg("main-singleplayer-map_scale"),
|
||||
gen_opts.scale
|
||||
))
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
);
|
||||
|
||||
if can_edit {
|
||||
gen_content.push(
|
||||
Slider::new(
|
||||
&mut self.map_vertical_scale,
|
||||
0.0..=160.0,
|
||||
gen_opts.scale * 10.0,
|
||||
move |s| message(WorldChange::Scale(s / 10.0)),
|
||||
)
|
||||
.height(SLIDER_HEIGHT)
|
||||
.style(style::slider::Style::images(
|
||||
imgs.slider_indicator,
|
||||
imgs.slider_range,
|
||||
SLIDER_BAR_PAD,
|
||||
SLIDER_CURSOR_SIZE,
|
||||
SLIDER_BAR_HEIGHT,
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if can_edit {
|
||||
gen_content.extend([
|
||||
Text::new(i18n.get_msg("main-singleplayer-map_shape"))
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
Row::with_children(
|
||||
self.shape_buttons
|
||||
.iter_mut()
|
||||
.map(|(shape, state)| {
|
||||
let color = if gen_opts.map_kind == shape {
|
||||
(97, 255, 18)
|
||||
} else {
|
||||
(97, 97, 25)
|
||||
};
|
||||
Button::new(
|
||||
state,
|
||||
Row::with_children(vec![
|
||||
Space::new(Length::FillPortion(5), Length::Units(0))
|
||||
.into(),
|
||||
Text::new(shape.to_string())
|
||||
.width(Length::FillPortion(95))
|
||||
.size(fonts.cyri.scale(14))
|
||||
.vertical_alignment(iced::VerticalAlignment::Center)
|
||||
.into(),
|
||||
])
|
||||
.align_items(Align::Center),
|
||||
)
|
||||
.style(
|
||||
style::button::Style::new(imgs.selection)
|
||||
.hover_image(imgs.selection_hover)
|
||||
.press_image(imgs.selection_press)
|
||||
.image_color(Rgba::new(color.0, color.1, color.2, 192)),
|
||||
)
|
||||
.width(Length::FillPortion(1))
|
||||
.min_height(18)
|
||||
.on_press(Message::WorldChanged(
|
||||
super::WorldsChange::CurrentWorldChange(
|
||||
WorldChange::MapKind(shape),
|
||||
),
|
||||
))
|
||||
.into()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.into(),
|
||||
]);
|
||||
} else {
|
||||
gen_content.push(
|
||||
Text::new(format!(
|
||||
"{}: {}",
|
||||
i18n.get_msg("main-singleplayer-map_shape"),
|
||||
gen_opts.map_kind,
|
||||
))
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
gen_content.push(
|
||||
Text::new(format!(
|
||||
"{}: {}",
|
||||
i18n.get_msg("main-singleplayer-map_erosion_quality"),
|
||||
gen_opts.erosion_quality
|
||||
))
|
||||
.size(SLIDER_TEXT_SIZE)
|
||||
.horizontal_alignment(iced::HorizontalAlignment::Center)
|
||||
.into(),
|
||||
);
|
||||
|
||||
if can_edit {
|
||||
gen_content.push(
|
||||
Slider::new(
|
||||
&mut self.map_erosion_quality,
|
||||
0.0..=20.0,
|
||||
gen_opts.erosion_quality * 10.0,
|
||||
move |s| message(WorldChange::ErosionQuality(s / 10.0)),
|
||||
)
|
||||
.height(SLIDER_HEIGHT)
|
||||
.style(style::slider::Style::images(
|
||||
imgs.slider_indicator,
|
||||
imgs.slider_range,
|
||||
SLIDER_BAR_PAD,
|
||||
SLIDER_CURSOR_SIZE,
|
||||
SLIDER_BAR_HEIGHT,
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut world_buttons = vec![];
|
||||
|
||||
if world.gen_opts.is_none() && can_edit {
|
||||
let create_custom = Container::new(neat_button(
|
||||
&mut self.regenerate_map,
|
||||
i18n.get_msg("main-singleplayer-create_custom"),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(Message::WorldChanged(
|
||||
super::WorldsChange::CurrentWorldChange(WorldChange::DefaultGenOps),
|
||||
)),
|
||||
))
|
||||
.center_x()
|
||||
.width(Length::FillPortion(1))
|
||||
.max_width(200);
|
||||
world_buttons.push(create_custom.into());
|
||||
}
|
||||
|
||||
if world.is_generated {
|
||||
let regenerate = Container::new(neat_button(
|
||||
&mut self.generate_map,
|
||||
i18n.get_msg("main-singleplayer-regenerate"),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(Message::WorldConfirmation(Confirmation::Regenerate(i))),
|
||||
))
|
||||
.center_x()
|
||||
.width(Length::FillPortion(1))
|
||||
.max_width(200);
|
||||
world_buttons.push(regenerate.into())
|
||||
}
|
||||
let delete = Container::new(neat_button(
|
||||
&mut self.delete_world,
|
||||
i18n.get_msg("main-singleplayer-delete"),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(Message::WorldConfirmation(Confirmation::Delete(i))),
|
||||
))
|
||||
.center_x()
|
||||
.width(Length::FillPortion(1))
|
||||
.max_width(200);
|
||||
|
||||
world_buttons.push(delete.into());
|
||||
|
||||
gen_content.push(Row::with_children(world_buttons).into());
|
||||
|
||||
let play_button = Container::new(neat_button(
|
||||
&mut self.play_button,
|
||||
i18n.get_msg(if world.is_generated || world.gen_opts.is_none() {
|
||||
"main-singleplayer-play"
|
||||
} else {
|
||||
"main-singleplayer-generate_and_play"
|
||||
}),
|
||||
FILL_FRAC_TWO,
|
||||
button_style,
|
||||
Some(Message::SingleplayerPlay),
|
||||
))
|
||||
.center_x()
|
||||
.max_width(200);
|
||||
|
||||
gen_content.push(play_button.into());
|
||||
|
||||
let gen_opts = Column::with_children(gen_content).align_items(Align::Center);
|
||||
|
||||
let opts_menu = BackgroundContainer::new(
|
||||
CompoundGraphic::from_graphics(vec![
|
||||
Graphic::image(imgs.banner_top, [138, 17], [0, 0]),
|
||||
Graphic::rect(Rgba::new(0, 0, 0, 230), [130, 300], [4, 17]),
|
||||
// TODO: use non image gradient
|
||||
Graphic::gradient(Rgba::new(0, 0, 0, 230), Rgba::zero(), [130, 50], [4, 182]),
|
||||
])
|
||||
.fix_aspect_ratio()
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill),
|
||||
gen_opts,
|
||||
)
|
||||
.padding(Padding::new().horizontal(5).top(15));
|
||||
|
||||
items.push(opts_menu.into());
|
||||
}
|
||||
|
||||
let all = Row::with_children(items)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill);
|
||||
|
||||
if let Some(confirmation) = self.confirmation.as_ref() {
|
||||
const FILL_FRAC_ONE: f32 = 0.77;
|
||||
|
||||
let (text, yes_msg, index) = match confirmation {
|
||||
Confirmation::Regenerate(i) => (
|
||||
"menu-singleplayer-confirm_regenerate",
|
||||
Message::WorldChanged(WorldsChange::Regenerate(*i)),
|
||||
i,
|
||||
),
|
||||
Confirmation::Delete(i) => (
|
||||
"menu-singleplayer-confirm_delete",
|
||||
Message::WorldChanged(WorldsChange::Delete(*i)),
|
||||
i,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(name) = worlds.worlds.get(*index).map(|world| &world.name) {
|
||||
let over_content = Column::with_children(vec![
|
||||
Text::new(i18n.get_msg_ctx(text, &i18n::fluent_args! { "world_name" => name }))
|
||||
.size(fonts.cyri.scale(24))
|
||||
.into(),
|
||||
Row::with_children(vec![
|
||||
neat_button(
|
||||
&mut self.no_button,
|
||||
i18n.get_msg("common-no").into_owned(),
|
||||
FILL_FRAC_ONE,
|
||||
button_style,
|
||||
Some(Message::WorldCancelConfirmation),
|
||||
),
|
||||
neat_button(
|
||||
&mut self.yes_button,
|
||||
i18n.get_msg("common-yes").into_owned(),
|
||||
FILL_FRAC_ONE,
|
||||
button_style,
|
||||
Some(yes_msg),
|
||||
),
|
||||
])
|
||||
.height(Length::Units(28))
|
||||
.spacing(30)
|
||||
.into(),
|
||||
])
|
||||
.align_items(Align::Center)
|
||||
.spacing(10);
|
||||
|
||||
let over = Container::new(over_content)
|
||||
.style(
|
||||
style::container::Style::color_with_double_cornerless_border(
|
||||
(0, 0, 0, 200).into(),
|
||||
(3, 4, 4, 255).into(),
|
||||
(28, 28, 22, 255).into(),
|
||||
),
|
||||
)
|
||||
.width(Length::Shrink)
|
||||
.height(Length::Shrink)
|
||||
.max_width(400)
|
||||
.max_height(500)
|
||||
.padding(24)
|
||||
.center_x()
|
||||
.center_y();
|
||||
|
||||
Overlay::new(over, all)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
} else {
|
||||
self.confirmation = None;
|
||||
all.into()
|
||||
}
|
||||
} else {
|
||||
all.into()
|
||||
}
|
||||
}
|
||||
}
|
@ -500,7 +500,7 @@ impl PlayState for SessionState {
|
||||
{
|
||||
// Update the Discord activity on client initialization
|
||||
#[cfg(feature = "singleplayer")]
|
||||
let singleplayer = global_state.singleplayer.is_some();
|
||||
let singleplayer = global_state.singleplayer.is_running();
|
||||
#[cfg(not(feature = "singleplayer"))]
|
||||
let singleplayer = false;
|
||||
|
||||
|
@ -742,7 +742,7 @@ impl SettingsChange {
|
||||
global_state.discord = Discord::start(&global_state.tokio_runtime);
|
||||
|
||||
#[cfg(feature = "singleplayer")]
|
||||
let singleplayer = global_state.singleplayer.is_some();
|
||||
let singleplayer = global_state.singleplayer.is_running();
|
||||
#[cfg(not(feature = "singleplayer"))]
|
||||
let singleplayer = false;
|
||||
|
||||
|
@ -1,162 +0,0 @@
|
||||
use common::clock::Clock;
|
||||
use crossbeam_channel::{bounded, unbounded, Receiver, Sender, TryRecvError};
|
||||
use server::{
|
||||
persistence::{DatabaseSettings, SqlLogMode},
|
||||
Error as ServerError, Event, Input, Server,
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
const TPS: u64 = 30;
|
||||
|
||||
/// Used to start and stop the background thread running the server
|
||||
/// when in singleplayer mode.
|
||||
pub struct Singleplayer {
|
||||
_server_thread: JoinHandle<()>,
|
||||
stop_server_s: Sender<()>,
|
||||
pub receiver: Receiver<Result<(), ServerError>>,
|
||||
// Wether the server is stopped or not
|
||||
paused: Arc<AtomicBool>,
|
||||
// Settings that the server was started with
|
||||
settings: server::Settings,
|
||||
}
|
||||
|
||||
impl Singleplayer {
|
||||
pub fn new(runtime: &Arc<Runtime>) -> Self {
|
||||
let (stop_server_s, stop_server_r) = unbounded();
|
||||
|
||||
// Determine folder to save server data in
|
||||
let server_data_dir = {
|
||||
let mut path = common_base::userdata_dir_workspace!();
|
||||
path.push("singleplayer");
|
||||
path
|
||||
};
|
||||
|
||||
// Create server
|
||||
let settings = server::Settings::singleplayer(&server_data_dir);
|
||||
let editable_settings = server::EditableSettings::singleplayer(&server_data_dir);
|
||||
|
||||
let settings2 = settings.clone();
|
||||
|
||||
// Relative to data_dir
|
||||
const PERSISTENCE_DB_DIR: &str = "saves";
|
||||
|
||||
let database_settings = DatabaseSettings {
|
||||
db_dir: server_data_dir.join(PERSISTENCE_DB_DIR),
|
||||
sql_log_mode: SqlLogMode::Disabled, /* Voxygen doesn't take in command-line arguments
|
||||
* so SQL logging can't be enabled for
|
||||
* singleplayer without changing this line
|
||||
* manually */
|
||||
};
|
||||
|
||||
let paused = Arc::new(AtomicBool::new(false));
|
||||
let paused1 = Arc::clone(&paused);
|
||||
|
||||
let (result_sender, result_receiver) = bounded(1);
|
||||
|
||||
let builder = thread::Builder::new().name("singleplayer-server-thread".into());
|
||||
let runtime = Arc::clone(runtime);
|
||||
let thread = builder
|
||||
.spawn(move || {
|
||||
trace!("starting singleplayer server thread");
|
||||
|
||||
let (server, init_result) = match Server::new(
|
||||
settings2,
|
||||
editable_settings,
|
||||
database_settings,
|
||||
&server_data_dir,
|
||||
runtime,
|
||||
) {
|
||||
Ok(server) => (Some(server), Ok(())),
|
||||
Err(err) => (None, Err(err)),
|
||||
};
|
||||
|
||||
match (result_sender.send(init_result), server) {
|
||||
(Err(e), _) => warn!(
|
||||
?e,
|
||||
"Failed to send singleplayer server initialization result. Most likely \
|
||||
the channel was closed by cancelling server creation. Stopping Server"
|
||||
),
|
||||
(Ok(()), None) => (),
|
||||
(Ok(()), Some(server)) => run_server(server, stop_server_r, paused1),
|
||||
}
|
||||
|
||||
trace!("ending singleplayer server thread");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Singleplayer {
|
||||
_server_thread: thread,
|
||||
stop_server_s,
|
||||
receiver: result_receiver,
|
||||
paused,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns reference to the settings the server was started with
|
||||
pub fn settings(&self) -> &server::Settings { &self.settings }
|
||||
|
||||
/// Returns wether or not the server is paused
|
||||
pub fn is_paused(&self) -> bool { self.paused.load(Ordering::SeqCst) }
|
||||
|
||||
/// Pauses if true is passed and unpauses if false (Does nothing if in that
|
||||
/// state already)
|
||||
pub fn pause(&self, state: bool) { self.paused.store(state, Ordering::SeqCst); }
|
||||
}
|
||||
|
||||
impl Drop for Singleplayer {
|
||||
fn drop(&mut self) {
|
||||
// Ignore the result
|
||||
let _ = self.stop_server_s.send(());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_server(mut server: Server, stop_server_r: Receiver<()>, paused: Arc<AtomicBool>) {
|
||||
info!("Starting server-cli...");
|
||||
|
||||
// Set up an fps clock
|
||||
let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64));
|
||||
|
||||
loop {
|
||||
// Check any event such as stopping and pausing
|
||||
match stop_server_r.try_recv() {
|
||||
Ok(()) => break,
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
Err(TryRecvError::Empty) => (),
|
||||
}
|
||||
|
||||
// Wait for the next tick.
|
||||
clock.tick();
|
||||
|
||||
// Skip updating the server if it's paused
|
||||
if paused.load(Ordering::SeqCst) && server.number_of_players() < 2 {
|
||||
continue;
|
||||
} else if server.number_of_players() > 1 {
|
||||
paused.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let events = server
|
||||
.tick(Input::default(), clock.dt())
|
||||
.expect("Failed to tick server!");
|
||||
|
||||
for event in events {
|
||||
match event {
|
||||
Event::ClientConnected { .. } => info!("Client connected!"),
|
||||
Event::ClientDisconnected { .. } => info!("Client disconnected!"),
|
||||
Event::Chat { entity: _, msg } => info!("[Client] {}", msg),
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the server after a tick.
|
||||
server.cleanup();
|
||||
}
|
||||
}
|
210
voxygen/src/singleplayer/mod.rs
Normal file
210
voxygen/src/singleplayer/mod.rs
Normal file
@ -0,0 +1,210 @@
|
||||
use common::clock::Clock;
|
||||
use crossbeam_channel::{bounded, unbounded, Receiver, Sender, TryRecvError};
|
||||
use server::{
|
||||
persistence::{DatabaseSettings, SqlLogMode},
|
||||
Error as ServerError, Event, Input, Server,
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
mod singleplayer_world;
|
||||
pub use singleplayer_world::{SingleplayerWorld, SingleplayerWorlds};
|
||||
|
||||
const TPS: u64 = 30;
|
||||
|
||||
/// Used to start and stop the background thread running the server
|
||||
/// when in singleplayer mode.
|
||||
pub struct Singleplayer {
|
||||
_server_thread: JoinHandle<()>,
|
||||
stop_server_s: Sender<()>,
|
||||
pub receiver: Receiver<Result<(), ServerError>>,
|
||||
// Wether the server is stopped or not
|
||||
paused: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Singleplayer {
|
||||
/// Returns wether or not the server is paused
|
||||
pub fn is_paused(&self) -> bool { self.paused.load(Ordering::SeqCst) }
|
||||
|
||||
/// Pauses if true is passed and unpauses if false (Does nothing if in that
|
||||
/// state already)
|
||||
pub fn pause(&self, state: bool) { self.paused.store(state, Ordering::SeqCst); }
|
||||
}
|
||||
|
||||
impl Drop for Singleplayer {
|
||||
fn drop(&mut self) {
|
||||
// Ignore the result
|
||||
let _ = self.stop_server_s.send(());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum SingleplayerState {
|
||||
#[default]
|
||||
None,
|
||||
Init(SingleplayerWorlds),
|
||||
Running(Singleplayer),
|
||||
}
|
||||
|
||||
impl SingleplayerState {
|
||||
pub fn init() -> Self {
|
||||
let dir = common_base::userdata_dir_workspace!();
|
||||
|
||||
Self::Init(SingleplayerWorlds::load(&dir))
|
||||
}
|
||||
|
||||
pub fn run(&mut self, runtime: &Arc<Runtime>) {
|
||||
if let Self::Init(worlds) = self {
|
||||
let Some(world) = worlds.current() else {
|
||||
error!("Failed to get the current world.");
|
||||
return;
|
||||
};
|
||||
let server_data_dir = world.path.clone();
|
||||
|
||||
let mut settings = server::Settings::singleplayer(&server_data_dir);
|
||||
let editable_settings = server::EditableSettings::singleplayer(&server_data_dir);
|
||||
|
||||
let file_opts = if let Some(gen_opts) = &world.gen_opts && !world.is_generated {
|
||||
server::FileOpts::Save(
|
||||
world.map_path.clone(),
|
||||
gen_opts.clone(),
|
||||
)
|
||||
} else {
|
||||
if !world.is_generated && world.gen_opts.is_none() {
|
||||
world.copy_default_world();
|
||||
}
|
||||
server::FileOpts::Load(world.map_path.clone())
|
||||
};
|
||||
|
||||
settings.map_file = Some(file_opts);
|
||||
settings.world_seed = world.seed;
|
||||
|
||||
let (stop_server_s, stop_server_r) = unbounded();
|
||||
|
||||
// Create server
|
||||
|
||||
// Relative to data_dir
|
||||
const PERSISTENCE_DB_DIR: &str = "saves";
|
||||
|
||||
let database_settings = DatabaseSettings {
|
||||
db_dir: server_data_dir.join(PERSISTENCE_DB_DIR),
|
||||
sql_log_mode: SqlLogMode::Disabled, /* Voxygen doesn't take in command-line
|
||||
* arguments
|
||||
* so SQL logging can't be enabled for
|
||||
* singleplayer without changing this line
|
||||
* manually */
|
||||
};
|
||||
|
||||
let paused = Arc::new(AtomicBool::new(false));
|
||||
let paused1 = Arc::clone(&paused);
|
||||
|
||||
let (result_sender, result_receiver) = bounded(1);
|
||||
|
||||
let builder = thread::Builder::new().name("singleplayer-server-thread".into());
|
||||
let runtime = Arc::clone(runtime);
|
||||
let thread = builder
|
||||
.spawn(move || {
|
||||
trace!("starting singleplayer server thread");
|
||||
|
||||
let (server, init_result) = match Server::new(
|
||||
settings,
|
||||
editable_settings,
|
||||
database_settings,
|
||||
&server_data_dir,
|
||||
runtime,
|
||||
) {
|
||||
Ok(server) => (Some(server), Ok(())),
|
||||
Err(err) => (None, Err(err)),
|
||||
};
|
||||
|
||||
match (result_sender.send(init_result), server) {
|
||||
(Err(e), _) => warn!(
|
||||
?e,
|
||||
"Failed to send singleplayer server initialization result. Most \
|
||||
likely the channel was closed by cancelling server creation. \
|
||||
Stopping Server"
|
||||
),
|
||||
(Ok(()), None) => (),
|
||||
(Ok(()), Some(server)) => run_server(server, stop_server_r, paused1),
|
||||
}
|
||||
|
||||
trace!("ending singleplayer server thread");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
*self = SingleplayerState::Running(Singleplayer {
|
||||
_server_thread: thread,
|
||||
stop_server_s,
|
||||
receiver: result_receiver,
|
||||
paused,
|
||||
});
|
||||
} else {
|
||||
error!("SingleplayerState::run was called, but singleplayer is already running!");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_running(&self) -> Option<&Singleplayer> {
|
||||
match self {
|
||||
SingleplayerState::Running(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_init(&self) -> Option<&SingleplayerWorlds> {
|
||||
match self {
|
||||
SingleplayerState::Init(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool { matches!(self, SingleplayerState::Running(_)) }
|
||||
}
|
||||
|
||||
fn run_server(mut server: Server, stop_server_r: Receiver<()>, paused: Arc<AtomicBool>) {
|
||||
info!("Starting server-cli...");
|
||||
|
||||
// Set up an fps clock
|
||||
let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64));
|
||||
|
||||
loop {
|
||||
// Check any event such as stopping and pausing
|
||||
match stop_server_r.try_recv() {
|
||||
Ok(()) => break,
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
Err(TryRecvError::Empty) => (),
|
||||
}
|
||||
|
||||
// Wait for the next tick.
|
||||
clock.tick();
|
||||
|
||||
// Skip updating the server if it's paused
|
||||
if paused.load(Ordering::SeqCst) && server.number_of_players() < 2 {
|
||||
continue;
|
||||
} else if server.number_of_players() > 1 {
|
||||
paused.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let events = server
|
||||
.tick(Input::default(), clock.dt())
|
||||
.expect("Failed to tick server!");
|
||||
|
||||
for event in events {
|
||||
match event {
|
||||
Event::ClientConnected { .. } => info!("Client connected!"),
|
||||
Event::ClientDisconnected { .. } => info!("Client disconnected!"),
|
||||
Event::Chat { entity: _, msg } => info!("[Client] {}", msg),
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the server after a tick.
|
||||
server.cleanup();
|
||||
}
|
||||
}
|
341
voxygen/src/singleplayer/singleplayer_world.rs
Normal file
341
voxygen/src/singleplayer/singleplayer_world.rs
Normal file
@ -0,0 +1,341 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use common::assets::ASSETS_PATH;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use server::{FileOpts, GenOpts, DEFAULT_WORLD_MAP};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
struct World0 {
|
||||
name: String,
|
||||
gen_opts: Option<GenOpts>,
|
||||
seed: u32,
|
||||
}
|
||||
|
||||
pub struct SingleplayerWorld {
|
||||
pub name: String,
|
||||
pub gen_opts: Option<GenOpts>,
|
||||
pub seed: u32,
|
||||
pub is_generated: bool,
|
||||
pub path: PathBuf,
|
||||
pub map_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SingleplayerWorld {
|
||||
pub fn copy_default_world(&self) {
|
||||
if let Err(e) = fs::copy(asset_path(DEFAULT_WORLD_MAP), &self.map_path) {
|
||||
println!("Error when trying to copy default world: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_map(path: &Path) -> Option<SingleplayerWorld> {
|
||||
let meta_path = path.join("meta.ron");
|
||||
|
||||
let Ok(f) = fs::File::open(&meta_path) else {
|
||||
error!("Failed to open {}", meta_path.to_string_lossy());
|
||||
return None;
|
||||
};
|
||||
|
||||
version::try_load(&f, path)
|
||||
}
|
||||
|
||||
fn write_world_meta(world: &SingleplayerWorld) {
|
||||
let path = &world.path;
|
||||
|
||||
if let Err(e) = fs::create_dir_all(path) {
|
||||
error!("Failed to create world folder: {e}");
|
||||
}
|
||||
|
||||
match fs::File::create(path.join("meta.ron")) {
|
||||
Ok(file) => {
|
||||
if let Err(e) = ron::ser::to_writer_pretty(
|
||||
file,
|
||||
&version::Current::from_world(world),
|
||||
ron::ser::PrettyConfig::new(),
|
||||
) {
|
||||
error!("Failed to create world meta file: {e}")
|
||||
}
|
||||
},
|
||||
Err(e) => error!("Failed to create world meta file: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn asset_path(asset: &str) -> PathBuf {
|
||||
let mut s = asset.replace('.', "/");
|
||||
s.push_str(".bin");
|
||||
ASSETS_PATH.join(s)
|
||||
}
|
||||
|
||||
fn migrate_old_singleplayer(from: &Path, to: &Path) {
|
||||
if fs::metadata(from).map_or(false, |meta| meta.is_dir()) {
|
||||
if let Err(e) = fs::rename(from, to) {
|
||||
error!("Failed to migrate singleplayer: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut seed = 0;
|
||||
let (map_file, gen_opts) = fs::read_to_string(to.join("server_config/settings.ron"))
|
||||
.ok()
|
||||
.and_then(|settings| {
|
||||
let settings: server::Settings = ron::from_str(&settings).ok()?;
|
||||
seed = settings.world_seed;
|
||||
Some(match settings.map_file? {
|
||||
FileOpts::LoadOrGenerate { name, opts, .. } => {
|
||||
(Some(PathBuf::from(name)), Some(opts))
|
||||
},
|
||||
FileOpts::Generate(opts) => (None, Some(opts)),
|
||||
FileOpts::LoadLegacy(_) => return None,
|
||||
FileOpts::Load(path) => (Some(path), None),
|
||||
FileOpts::LoadAsset(asset) => (Some(asset_path(&asset)), None),
|
||||
FileOpts::Save(_, gen_opts) => (None, Some(gen_opts)),
|
||||
})
|
||||
})
|
||||
.unwrap_or((Some(asset_path(DEFAULT_WORLD_MAP)), None));
|
||||
|
||||
let map_path = to.join("map.bin");
|
||||
if let Some(map_file) = map_file {
|
||||
if let Err(err) = fs::copy(map_file, &map_path) {
|
||||
error!("Failed to copy map file to singleplayer world: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
write_world_meta(&SingleplayerWorld {
|
||||
name: "singleplayer world".to_string(),
|
||||
gen_opts,
|
||||
seed,
|
||||
path: to.to_path_buf(),
|
||||
// Isn't persisted so doesn't matter what it's set to.
|
||||
is_generated: false,
|
||||
map_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn load_worlds(path: &Path) -> Vec<SingleplayerWorld> {
|
||||
let Ok(paths) = fs::read_dir(path) else {
|
||||
let _ = fs::create_dir_all(path);
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
paths
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if entry.file_type().ok()?.is_dir() {
|
||||
let path = entry.path();
|
||||
load_map(&path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SingleplayerWorlds {
|
||||
pub worlds: Vec<SingleplayerWorld>,
|
||||
pub current: Option<usize>,
|
||||
worlds_folder: PathBuf,
|
||||
}
|
||||
|
||||
impl SingleplayerWorlds {
|
||||
pub fn load(userdata_folder: &Path) -> SingleplayerWorlds {
|
||||
let worlds_folder = userdata_folder.join("singleplayer_worlds");
|
||||
|
||||
if let Err(e) = fs::create_dir_all(&worlds_folder) {
|
||||
error!("Failed to create singleplayer worlds folder: {e}");
|
||||
}
|
||||
|
||||
migrate_old_singleplayer(
|
||||
&userdata_folder.join("singleplayer"),
|
||||
&worlds_folder.join("singleplayer"),
|
||||
);
|
||||
|
||||
let worlds = load_worlds(&worlds_folder);
|
||||
|
||||
SingleplayerWorlds {
|
||||
worlds,
|
||||
current: None,
|
||||
worlds_folder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_map_file(&mut self, map: usize) {
|
||||
let w = &mut self.worlds[map];
|
||||
if w.is_generated {
|
||||
// We don't care about the result here since we aren't sure the file exists.
|
||||
let _ = fs::remove_file(&w.map_path);
|
||||
}
|
||||
w.is_generated = false;
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, idx: usize) {
|
||||
if let Some(ref mut i) = self.current {
|
||||
match (*i).cmp(&idx) {
|
||||
std::cmp::Ordering::Less => {},
|
||||
std::cmp::Ordering::Equal => self.current = None,
|
||||
std::cmp::Ordering::Greater => *i -= 1,
|
||||
}
|
||||
}
|
||||
let _ = fs::remove_dir_all(&self.worlds[idx].path);
|
||||
self.worlds.remove(idx);
|
||||
}
|
||||
|
||||
fn world_folder_name(&self) -> String {
|
||||
use chrono::{Datelike, Timelike};
|
||||
let now = chrono::Local::now().naive_local();
|
||||
let name = format!(
|
||||
"world-{}-{}-{}-{}_{}_{}_{}",
|
||||
now.year(),
|
||||
now.month(),
|
||||
now.day(),
|
||||
now.hour(),
|
||||
now.minute(),
|
||||
now.second(),
|
||||
now.timestamp_subsec_millis()
|
||||
);
|
||||
|
||||
let mut test_name = name.clone();
|
||||
let mut i = 0;
|
||||
'fail: loop {
|
||||
for world in self.worlds.iter() {
|
||||
if world.path.ends_with(&test_name) {
|
||||
test_name = name.clone();
|
||||
test_name.push('_');
|
||||
test_name.push_str(&i.to_string());
|
||||
i += 1;
|
||||
continue 'fail;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
test_name
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&SingleplayerWorld> {
|
||||
self.current.and_then(|i| self.worlds.get(i))
|
||||
}
|
||||
|
||||
pub fn new_world(&mut self) {
|
||||
let folder_name = self.world_folder_name();
|
||||
let path = self.worlds_folder.join(folder_name);
|
||||
|
||||
let new_world = SingleplayerWorld {
|
||||
name: "New World".to_string(),
|
||||
gen_opts: None,
|
||||
seed: 0,
|
||||
is_generated: false,
|
||||
map_path: path.join("map.bin"),
|
||||
path,
|
||||
};
|
||||
|
||||
write_world_meta(&new_world);
|
||||
|
||||
self.worlds.push(new_world)
|
||||
}
|
||||
|
||||
pub fn save_current_meta(&self) {
|
||||
if let Some(world) = self.current() {
|
||||
write_world_meta(world);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod version {
|
||||
use std::any::{type_name, Any};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type Current = V1;
|
||||
|
||||
type LoadWorldFn<R> =
|
||||
fn(R, &Path) -> Result<SingleplayerWorld, (&'static str, ron::de::SpannedError)>;
|
||||
fn loaders<'a, R: std::io::Read + Clone>() -> &'a [LoadWorldFn<R>] {
|
||||
// Step [4]
|
||||
&[load_raw::<V1, _>]
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct V1 {
|
||||
#[serde(deserialize_with = "version::<_, 1>")]
|
||||
version: u64,
|
||||
name: String,
|
||||
gen_opts: Option<GenOpts>,
|
||||
seed: u32,
|
||||
}
|
||||
|
||||
impl V1 {
|
||||
/// This function is only needed for the current version
|
||||
pub fn from_world(world: &SingleplayerWorld) -> Self {
|
||||
V1 {
|
||||
version: 1,
|
||||
name: world.name.clone(),
|
||||
gen_opts: world.gen_opts.clone(),
|
||||
seed: world.seed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToWorld for V1 {
|
||||
fn to_world(self, path: PathBuf) -> SingleplayerWorld {
|
||||
let map_path = path.join("map.bin");
|
||||
let is_generated = fs::metadata(&map_path).is_ok_and(|f| f.is_file());
|
||||
|
||||
SingleplayerWorld {
|
||||
name: self.name,
|
||||
gen_opts: self.gen_opts,
|
||||
seed: self.seed,
|
||||
is_generated,
|
||||
path,
|
||||
map_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
fn version<'de, D: serde::Deserializer<'de>, const V: u64>(de: D) -> Result<u64, D::Error> {
|
||||
u64::deserialize(de).and_then(|x| {
|
||||
if x == V {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err(serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Unsigned(x),
|
||||
&"incorrect magic/version bytes",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
trait ToWorld {
|
||||
fn to_world(self, path: PathBuf) -> SingleplayerWorld;
|
||||
}
|
||||
|
||||
fn load_raw<RawWorld: Any + ToWorld + DeserializeOwned, R: std::io::Read + Clone>(
|
||||
reader: R,
|
||||
path: &Path,
|
||||
) -> Result<SingleplayerWorld, (&'static str, ron::de::SpannedError)> {
|
||||
ron::de::from_reader::<_, RawWorld>(reader)
|
||||
.map(|s| s.to_world(path.to_path_buf()))
|
||||
.map_err(|e| (type_name::<RawWorld>(), e))
|
||||
}
|
||||
|
||||
pub fn try_load<R: std::io::Read + Clone>(reader: R, path: &Path) -> Option<SingleplayerWorld> {
|
||||
loaders()
|
||||
.iter()
|
||||
.find_map(|load_raw| match load_raw(reader.clone(), path) {
|
||||
Ok(chunk) => Some(chunk),
|
||||
Err((raw_name, e)) => {
|
||||
error!(
|
||||
"Attempt to load chunk with raw format `{}` failed: {:?}",
|
||||
raw_name, e
|
||||
);
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
@ -166,17 +166,17 @@ macro_rules! image_ids {
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! image_ids_ice {
|
||||
($($v:vis struct $Ids:ident { $( <$T:ty> $( $name:ident: $specifier:expr ),* $(,)? )* })*) => {
|
||||
($($v:vis struct $Ids:ident { $( <$T:ty> $( $(#[$($attribute:tt)*])? $name:ident: $specifier:expr ),* $(,)? )* })*) => {
|
||||
$(
|
||||
$v struct $Ids {
|
||||
$($( $v $name: $crate::ui::GraphicId, )*)*
|
||||
$($( $(#[$($attribute)*])? $v $name: $crate::ui::GraphicId, )*)*
|
||||
}
|
||||
|
||||
impl $Ids {
|
||||
pub fn load(ui: &mut $crate::ui::ice::IcedUi) -> Result<Self, common::assets::Error> {
|
||||
use $crate::ui::img_ids::GraphicCreator;
|
||||
Ok(Self {
|
||||
$($( $name: ui.add_graphic(<$T as GraphicCreator>::new_graphic($specifier)?), )*)*
|
||||
$($( $(#[$($attribute)*])? $name: ui.add_graphic(<$T as GraphicCreator>::new_graphic($specifier)?), )*)*
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use common::{
|
||||
},
|
||||
vol::RectVolSize,
|
||||
};
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
// use faster::*;
|
||||
use itertools::izip;
|
||||
use noise::NoiseFn;
|
||||
@ -2640,6 +2640,13 @@ pub fn do_erosion(
|
||||
|
||||
(0..n_steps).for_each(|i| {
|
||||
debug!("Erosion iteration #{:?}", i);
|
||||
|
||||
// Print out the percentage complete. Do this at most 20 times.
|
||||
if i % std::cmp::max(n_steps / 20, 1) == 0 {
|
||||
let pct = (i as f64 / n_steps as f64) * 100.0;
|
||||
info!("{:.2}% complete", pct);
|
||||
}
|
||||
|
||||
erode(
|
||||
map_size_lg,
|
||||
&mut h,
|
||||
|
@ -43,6 +43,7 @@ use common::{
|
||||
calendar::Calendar,
|
||||
grid::Grid,
|
||||
lottery::Lottery,
|
||||
resources::MapKind,
|
||||
spiral::Spiral2d,
|
||||
store::{Id, Store},
|
||||
terrain::{
|
||||
@ -71,7 +72,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use tracing::{debug, warn};
|
||||
use tracing::{debug, info, warn};
|
||||
use vek::*;
|
||||
|
||||
/// Default base two logarithm of the world size, in chunks, per dimension.
|
||||
@ -138,22 +139,22 @@ pub(crate) struct GenCtx {
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct SizeOpts {
|
||||
x_lg: u32,
|
||||
y_lg: u32,
|
||||
scale: f64,
|
||||
pub struct GenOpts {
|
||||
pub x_lg: u32,
|
||||
pub y_lg: u32,
|
||||
pub scale: f64,
|
||||
pub map_kind: MapKind,
|
||||
pub erosion_quality: f32,
|
||||
}
|
||||
|
||||
impl SizeOpts {
|
||||
pub fn new(x_lg: u32, y_lg: u32, scale: f64) -> Self { Self { x_lg, y_lg, scale } }
|
||||
}
|
||||
|
||||
impl Default for SizeOpts {
|
||||
impl Default for GenOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x_lg: 10,
|
||||
y_lg: 10,
|
||||
scale: 2.0,
|
||||
map_kind: MapKind::Square,
|
||||
erosion_quality: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,17 +163,17 @@ impl Default for SizeOpts {
|
||||
pub enum FileOpts {
|
||||
/// If set, generate the world map and do not try to save to or load from
|
||||
/// file (default).
|
||||
Generate(SizeOpts),
|
||||
Generate(GenOpts),
|
||||
/// If set, generate the world map and save the world file (path is created
|
||||
/// the same way screenshot paths are).
|
||||
Save(SizeOpts),
|
||||
Save(PathBuf, GenOpts),
|
||||
/// Combination of Save and Load.
|
||||
/// Load map if exists or generate the world map and save the
|
||||
/// world file.
|
||||
LoadOrGenerate {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
opts: SizeOpts,
|
||||
opts: GenOpts,
|
||||
#[serde(default)]
|
||||
overwrite: bool,
|
||||
},
|
||||
@ -192,13 +193,15 @@ pub enum FileOpts {
|
||||
}
|
||||
|
||||
impl Default for FileOpts {
|
||||
fn default() -> Self { Self::Generate(SizeOpts::default()) }
|
||||
fn default() -> Self { Self::Generate(GenOpts::default()) }
|
||||
}
|
||||
|
||||
impl FileOpts {
|
||||
fn load_content(&self) -> (Option<ModernMap>, MapSizeLg, f64) {
|
||||
fn load_content(&self) -> (Option<ModernMap>, MapSizeLg, GenOpts) {
|
||||
let parsed_world_file = self.try_load_map();
|
||||
|
||||
let mut gen_opts = self.gen_opts().unwrap_or_default();
|
||||
|
||||
let map_size_lg = if let Some(map) = &parsed_world_file {
|
||||
MapSizeLg::new(map.map_size_lg)
|
||||
.expect("World size of loaded map does not satisfy invariants.")
|
||||
@ -214,21 +217,26 @@ impl FileOpts {
|
||||
//
|
||||
// FIXME: This is a hack! At some point we will have a more principled way of
|
||||
// dealing with this.
|
||||
let default_continent_scale_hack = 2.0/*4.0*/;
|
||||
let continent_scale_hack = if let Some(map) = &parsed_world_file {
|
||||
map.continent_scale_hack
|
||||
} else {
|
||||
self.continent_scale_hack()
|
||||
.unwrap_or(default_continent_scale_hack)
|
||||
if let Some(map) = &parsed_world_file {
|
||||
gen_opts.scale = map.continent_scale_hack;
|
||||
};
|
||||
|
||||
(parsed_world_file, map_size_lg, continent_scale_hack)
|
||||
(parsed_world_file, map_size_lg, gen_opts)
|
||||
}
|
||||
|
||||
fn gen_opts(&self) -> Option<GenOpts> {
|
||||
match self {
|
||||
Self::Generate(opts) | Self::Save(_, opts) | Self::LoadOrGenerate { opts, .. } => {
|
||||
Some(opts.clone())
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this should return Option so that caller can choose fallback
|
||||
fn map_size(&self) -> MapSizeLg {
|
||||
match self {
|
||||
Self::Generate(opts) | Self::Save(opts) | Self::LoadOrGenerate { opts, .. } => {
|
||||
Self::Generate(opts) | Self::Save(_, opts) | Self::LoadOrGenerate { opts, .. } => {
|
||||
MapSizeLg::new(Vec2 {
|
||||
x: opts.x_lg,
|
||||
y: opts.y_lg,
|
||||
@ -242,15 +250,6 @@ impl FileOpts {
|
||||
}
|
||||
}
|
||||
|
||||
fn continent_scale_hack(&self) -> Option<f64> {
|
||||
match self {
|
||||
Self::Generate(opts) | Self::Save(opts) | Self::LoadOrGenerate { opts, .. } => {
|
||||
Some(opts.scale)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This should probably return a Result, so that caller can choose
|
||||
// whether to log error
|
||||
fn try_load_map(&self) -> Option<ModernMap> {
|
||||
@ -357,7 +356,9 @@ impl FileOpts {
|
||||
// NOTE: we intentionally use pattern-matching here to get
|
||||
// options, so that when gen opts get another field, compiler
|
||||
// will force you to update following logic
|
||||
let SizeOpts { x_lg, y_lg, scale } = opts;
|
||||
let GenOpts {
|
||||
x_lg, y_lg, scale, ..
|
||||
} = opts;
|
||||
let map = match map {
|
||||
WorldFile::Veloren0_7_0(map) => map,
|
||||
WorldFile::Veloren0_5_0(_) => {
|
||||
@ -403,25 +404,16 @@ impl FileOpts {
|
||||
}
|
||||
|
||||
fn map_path(&self) -> Option<PathBuf> {
|
||||
const MAP_DIR: &str = "./maps";
|
||||
// TODO: Work out a nice bincode file extension.
|
||||
let file_name = match self {
|
||||
Self::Save { .. } => {
|
||||
use std::time::SystemTime;
|
||||
|
||||
Some(format!(
|
||||
"map_{}.bin",
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0)
|
||||
))
|
||||
match self {
|
||||
Self::Save(path, _) => Some(PathBuf::from(&path)),
|
||||
Self::LoadOrGenerate { name, .. } => {
|
||||
const MAP_DIR: &str = "./maps";
|
||||
let file_name = format!("{}.bin", name);
|
||||
Some(std::path::Path::new(MAP_DIR).join(file_name))
|
||||
},
|
||||
Self::LoadOrGenerate { name, .. } => Some(format!("{}.bin", name)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
file_name.map(|name| std::path::Path::new(MAP_DIR).join(name))
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, map: &WorldFile) {
|
||||
@ -452,6 +444,9 @@ impl FileOpts {
|
||||
if let Err(e) = bincode::serialize_into(writer, map) {
|
||||
warn!(?e, "Couldn't write map");
|
||||
}
|
||||
if let Ok(p) = std::fs::canonicalize(path) {
|
||||
info!("Map saved at {}", p.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -674,13 +669,13 @@ impl WorldSim {
|
||||
let world_file = opts.world_file;
|
||||
|
||||
// Parse out the contents of various map formats into the values we need.
|
||||
let (parsed_world_file, map_size_lg, continent_scale_hack) = world_file.load_content();
|
||||
let (parsed_world_file, map_size_lg, gen_opts) = world_file.load_content();
|
||||
// Currently only used with LoadOrGenerate to know if we need to
|
||||
// overwrite world file
|
||||
let fresh = parsed_world_file.is_none();
|
||||
|
||||
let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
|
||||
let continent_scale = continent_scale_hack
|
||||
let continent_scale = gen_opts.scale
|
||||
* 5_000.0f64
|
||||
.div(32.0)
|
||||
.mul(TerrainChunkSize::RECT_SIZE.x as f64);
|
||||
@ -688,6 +683,8 @@ impl WorldSim {
|
||||
let uplift_scale = 128.0;
|
||||
let uplift_turb_scale = uplift_scale / 4.0;
|
||||
|
||||
info!("Starting world generation");
|
||||
|
||||
// NOTE: Changing order will significantly change WorldGen, so try not to!
|
||||
let gen_ctx = GenCtx {
|
||||
turb_x_nz: SuperSimplex::new().set_seed(rng.gen()),
|
||||
@ -759,7 +756,7 @@ impl WorldSim {
|
||||
// Suppose the old world has grid spacing Δx' = Δy', new Δx = Δy.
|
||||
// We define grid_scale such that Δx = height_scale * Δx' ⇒
|
||||
// grid_scale = Δx / Δx'.
|
||||
let grid_scale = 1.0f64 / (4.0 / continent_scale_hack)/*1.0*/;
|
||||
let grid_scale = 1.0f64 / (4.0 / gen_opts.scale)/*1.0*/;
|
||||
|
||||
// Now, suppose we want to generate a world with "similar" topography, defined
|
||||
// in this case as having roughly equal slopes at steady state, with the
|
||||
@ -801,7 +798,7 @@ impl WorldSim {
|
||||
// grid (when a chunk isn't available).
|
||||
let n_approx = 1.0;
|
||||
let max_erosion_per_delta_t = 64.0 * delta_t_scale(n_approx);
|
||||
let n_steps = 100;
|
||||
let n_steps = (100.0 * gen_opts.erosion_quality) as usize;
|
||||
let n_small_steps = 0;
|
||||
let n_post_load_steps = 0;
|
||||
|
||||
@ -821,18 +818,44 @@ impl WorldSim {
|
||||
let ((alt_base, _), (chaos, _)) = threadpool.join(
|
||||
|| {
|
||||
uniform_noise(map_size_lg, |_, wposf| {
|
||||
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale (multiplied
|
||||
// value is from -0.35 * (CONFIG.mountain_scale * 1.05) to
|
||||
// 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675 to
|
||||
// 0.3325).
|
||||
Some(
|
||||
(gen_ctx
|
||||
.alt_nz
|
||||
.get((wposf.div(10_000.0)).into_array())
|
||||
.clamp(-1.0, 1.0))
|
||||
.sub(0.05)
|
||||
.mul(0.35),
|
||||
)
|
||||
match gen_opts.map_kind {
|
||||
MapKind::Square => {
|
||||
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale
|
||||
// (multiplied value is from -0.35 *
|
||||
// (CONFIG.mountain_scale * 1.05) to
|
||||
// 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675
|
||||
// to 0.3325).
|
||||
Some(
|
||||
(gen_ctx
|
||||
.alt_nz
|
||||
.get((wposf.div(10_000.0)).into_array())
|
||||
.min(1.0)
|
||||
.max(-1.0))
|
||||
.sub(0.05)
|
||||
.mul(0.35),
|
||||
)
|
||||
},
|
||||
MapKind::Circle => {
|
||||
let world_sizef = map_size_lg.chunks().map(|e| e as f64)
|
||||
* TerrainChunkSize::RECT_SIZE.map(|e| e as f64);
|
||||
Some(
|
||||
(gen_ctx
|
||||
.alt_nz
|
||||
.get((wposf.div(5_000.0 * gen_opts.scale)).into_array())
|
||||
.min(1.0)
|
||||
.max(-1.0))
|
||||
.add(
|
||||
0.2 - ((wposf / world_sizef) * 2.0 - 1.0)
|
||||
.magnitude_squared()
|
||||
.powf(0.75)
|
||||
.clamped(0.0, 1.0)
|
||||
.powf(1.0)
|
||||
* 0.6,
|
||||
)
|
||||
.mul(0.5),
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
|| {
|
||||
@ -1285,7 +1308,7 @@ impl WorldSim {
|
||||
// Save map, if necessary.
|
||||
// NOTE: We wll always save a map with latest version.
|
||||
let map = WorldFile::new(ModernMap {
|
||||
continent_scale_hack,
|
||||
continent_scale_hack: gen_opts.scale,
|
||||
map_size_lg: map_size_lg.vec(),
|
||||
alt,
|
||||
basement,
|
||||
@ -1415,7 +1438,7 @@ impl WorldSim {
|
||||
|
||||
let rivers = get_rivers(
|
||||
map_size_lg,
|
||||
continent_scale_hack,
|
||||
gen_opts.scale,
|
||||
&water_alt_pos,
|
||||
&water_alt,
|
||||
&dh,
|
||||
|
Loading…
Reference in New Issue
Block a user