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)
|
- 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
|
- 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
|
- 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
|
### Changed
|
||||||
|
|
||||||
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -7231,6 +7231,7 @@ dependencies = [
|
|||||||
"egui_wgpu_backend",
|
"egui_wgpu_backend",
|
||||||
"egui_winit_platform",
|
"egui_winit_platform",
|
||||||
"enum-iterator 1.4.1",
|
"enum-iterator 1.4.1",
|
||||||
|
"enum-map",
|
||||||
"etagere",
|
"etagere",
|
||||||
"euc",
|
"euc",
|
||||||
"gilrs",
|
"gilrs",
|
||||||
|
@ -32,6 +32,23 @@ main-login_process =
|
|||||||
You can create an account over at
|
You can create an account over at
|
||||||
|
|
||||||
https://veloren.net/account/.
|
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-server_not_found = Server not found
|
||||||
main-login-authentication_error = Auth error on server
|
main-login-authentication_error = Auth error on server
|
||||||
main-login-internal_error = Internal error on client (most likely, player character was deleted)
|
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() }
|
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
|
/// List of which players are using client-authoratative vs server-authoratative
|
||||||
/// physics, as a stop-gap until we can use server-authoratative physics for
|
/// physics, as a stop-gap until we can use server-authoratative physics for
|
||||||
/// everyone
|
/// everyone
|
||||||
|
@ -55,7 +55,11 @@ impl Site {
|
|||||||
.get(*faction)
|
.get(*faction)
|
||||||
.map_or(false, |f| f.good_or_evil == good_or_evil)
|
.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)
|
.map(|(_, faction)| *faction)
|
||||||
}),
|
}),
|
||||||
population: Default::default(),
|
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
|
// 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())
|
.filter(|(other_id, _, other_site2)| *other_id != site_id && other_site2.plots().len() > site2.plots().len())
|
||||||
.collect::<Vec<_>>();
|
.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;
|
let mut max_size = 0;
|
||||||
// Remove sites that aren't in increasing order of size (Stalin sort?!)
|
// Remove sites that aren't in increasing order of size (Stalin sort?!)
|
||||||
other_sites.retain(|(_, _, other_site2)| {
|
other_sites.retain(|(_, _, other_site2)| {
|
||||||
|
@ -131,8 +131,8 @@ use {
|
|||||||
use crate::persistence::character_loader::CharacterScreenResponseKind;
|
use crate::persistence::character_loader::CharacterScreenResponseKind;
|
||||||
use common::comp::Anchor;
|
use common::comp::Anchor;
|
||||||
#[cfg(feature = "worldgen")]
|
#[cfg(feature = "worldgen")]
|
||||||
use world::{
|
pub use world::{
|
||||||
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
|
sim::{FileOpts, GenOpts, WorldOpts, DEFAULT_WORLD_MAP},
|
||||||
IndexOwned, World,
|
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.";
|
successfully migrate to the latest version.";
|
||||||
|
|
||||||
/// Combines all the editable settings into one struct that is stored in the ecs
|
/// Combines all the editable settings into one struct that is stored in the ecs
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct EditableSettings {
|
pub struct EditableSettings {
|
||||||
pub whitelist: Whitelist,
|
pub whitelist: Whitelist,
|
||||||
pub banlist: Banlist,
|
pub banlist: Banlist,
|
||||||
|
@ -134,6 +134,7 @@ itertools = { workspace = true }
|
|||||||
|
|
||||||
# Discord RPC
|
# Discord RPC
|
||||||
discord-sdk = { version = "0.3.0", optional = true }
|
discord-sdk = { version = "0.3.0", optional = true }
|
||||||
|
enum-map = "2.5.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
dispatch = "0.1.4"
|
dispatch = "0.1.4"
|
||||||
|
@ -42,6 +42,8 @@ pub mod window;
|
|||||||
|
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
use crate::singleplayer::Singleplayer;
|
use crate::singleplayer::Singleplayer;
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
use crate::singleplayer::SingleplayerState;
|
||||||
#[cfg(feature = "egui-ui")]
|
#[cfg(feature = "egui-ui")]
|
||||||
use crate::ui::egui::EguiState;
|
use crate::ui::egui::EguiState;
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -74,7 +76,7 @@ pub struct GlobalState {
|
|||||||
pub info_message: Option<String>,
|
pub info_message: Option<String>,
|
||||||
pub clock: Clock,
|
pub clock: Clock,
|
||||||
#[cfg(feature = "singleplayer")]
|
#[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
|
// TODO: redo this so that the watcher doesn't have to exist for reloading to occur
|
||||||
pub i18n: LocalizationHandle,
|
pub i18n: LocalizationHandle,
|
||||||
pub clipboard: iced_winit::Clipboard,
|
pub clipboard: iced_winit::Clipboard,
|
||||||
@ -102,7 +104,7 @@ impl GlobalState {
|
|||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
pub fn paused(&self) -> bool {
|
pub fn paused(&self) -> bool {
|
||||||
self.singleplayer
|
self.singleplayer
|
||||||
.as_ref()
|
.as_running()
|
||||||
.map_or(false, Singleplayer::is_paused)
|
.map_or(false, Singleplayer::is_paused)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,10 +112,10 @@ impl GlobalState {
|
|||||||
pub fn paused(&self) -> bool { false }
|
pub fn paused(&self) -> bool { false }
|
||||||
|
|
||||||
#[cfg(feature = "singleplayer")]
|
#[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")]
|
#[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
|
// 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);
|
common_base::tracy_client::ProfiledAllocator::new(std::alloc::System, 128);
|
||||||
|
|
||||||
use i18n::{self, LocalizationHandle};
|
use i18n::{self, LocalizationHandle};
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
use veloren_voxygen::singleplayer::SingleplayerState;
|
||||||
use veloren_voxygen::{
|
use veloren_voxygen::{
|
||||||
audio::AudioFrontend,
|
audio::AudioFrontend,
|
||||||
panic_handler,
|
panic_handler,
|
||||||
@ -223,7 +225,7 @@ fn main() {
|
|||||||
settings,
|
settings,
|
||||||
info_message: None,
|
info_message: None,
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
singleplayer: None,
|
singleplayer: SingleplayerState::None,
|
||||||
i18n,
|
i18n,
|
||||||
clipboard,
|
clipboard,
|
||||||
clear_shadows_next_frame: false,
|
clear_shadows_next_frame: false,
|
||||||
|
@ -1325,36 +1325,27 @@ impl Controls {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let right_column_content = if character_id.is_none() {
|
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_sz = Vec2::new(500, 500);
|
||||||
let map_img = Image::new(self.map_img)
|
let map_img = Image::new(self.map_img)
|
||||||
.height(Length::Units(map_sz.x))
|
.height(Length::Units(map_sz.x))
|
||||||
.width(Length::Units(map_sz.y));
|
.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 {
|
/* .stroke(Stroke {
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
}) */;
|
}) */
|
||||||
//TODO: Add text-outline here whenever we updated iced to a version supporting
|
//TODO: Add text-outline here whenever we updated iced to a version supporting
|
||||||
// this
|
// this
|
||||||
|
|
||||||
let map = if let Some(info) = self.possible_starting_sites.get(*start_site_idx)
|
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
|
let pos_frac = info
|
||||||
.wpos
|
.wpos
|
||||||
.map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
|
.map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
|
||||||
@ -1388,6 +1379,15 @@ impl Controls {
|
|||||||
if self.possible_starting_sites.is_empty() {
|
if self.possible_starting_sites.is_empty() {
|
||||||
vec![map]
|
vec![map]
|
||||||
} else {
|
} 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![
|
let site_buttons = Row::with_children(vec![
|
||||||
neat_button(
|
neat_button(
|
||||||
prev_starting_site_button,
|
prev_starting_site_button,
|
||||||
@ -1972,9 +1972,9 @@ impl CharSelectionUi {
|
|||||||
let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
|
let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
|
||||||
|
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
let default_name = match global_state.singleplayer {
|
let default_name = match global_state.singleplayer.is_running() {
|
||||||
Some(_) => String::new(),
|
true => String::new(),
|
||||||
None => global_state.settings.networking.username.clone(),
|
false => global_state.settings.networking.username.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "singleplayer"))]
|
#[cfg(not(feature = "singleplayer"))]
|
||||||
|
@ -4,7 +4,7 @@ mod ui;
|
|||||||
|
|
||||||
use super::char_selection::CharSelectionState;
|
use super::char_selection::CharSelectionState;
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
use crate::singleplayer::Singleplayer;
|
use crate::singleplayer::SingleplayerState;
|
||||||
use crate::{
|
use crate::{
|
||||||
render::{Drawer, GlobalsBindGroup},
|
render::{Drawer, GlobalsBindGroup},
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
@ -73,7 +73,7 @@ impl PlayState for MainMenuState {
|
|||||||
// Reset singleplayer server if it was running already
|
// Reset singleplayer server if it was running already
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
{
|
{
|
||||||
global_state.singleplayer = None;
|
global_state.singleplayer = SingleplayerState::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated localization in case the selected language was changed
|
// Updated localization in case the selected language was changed
|
||||||
@ -97,7 +97,7 @@ impl PlayState for MainMenuState {
|
|||||||
// Poll server creation
|
// Poll server creation
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
{
|
{
|
||||||
if let Some(singleplayer) = &global_state.singleplayer {
|
if let Some(singleplayer) = global_state.singleplayer.as_running() {
|
||||||
match singleplayer.receiver.try_recv() {
|
match singleplayer.receiver.try_recv() {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
// Attempt login after the server is finished initializing
|
// Attempt login after the server is finished initializing
|
||||||
@ -113,7 +113,7 @@ impl PlayState for MainMenuState {
|
|||||||
},
|
},
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
error!(?e, "Could not start server");
|
error!(?e, "Could not start server");
|
||||||
global_state.singleplayer = None;
|
global_state.singleplayer = SingleplayerState::None;
|
||||||
self.init = InitState::None;
|
self.init = InitState::None;
|
||||||
self.main_menu_ui.cancel_connection();
|
self.main_menu_ui.cancel_connection();
|
||||||
let server_err = match e {
|
let server_err = match e {
|
||||||
@ -332,7 +332,7 @@ impl PlayState for MainMenuState {
|
|||||||
// blocking TODO fix when the network rework happens
|
// blocking TODO fix when the network rework happens
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
{
|
{
|
||||||
global_state.singleplayer = None;
|
global_state.singleplayer = SingleplayerState::None;
|
||||||
}
|
}
|
||||||
self.init = InitState::None;
|
self.init = InitState::None;
|
||||||
self.main_menu_ui.cancel_connection();
|
self.main_menu_ui.cancel_connection();
|
||||||
@ -351,9 +351,32 @@ impl PlayState for MainMenuState {
|
|||||||
},
|
},
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
MainMenuEvent::StartSingleplayer => {
|
MainMenuEvent::StartSingleplayer => {
|
||||||
let singleplayer = Singleplayer::new(&global_state.tokio_runtime);
|
global_state.singleplayer.run(&global_state.tokio_runtime);
|
||||||
|
},
|
||||||
global_state.singleplayer = Some(singleplayer);
|
#[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,
|
MainMenuEvent::Quit => return PlayStateResult::Shutdown,
|
||||||
// Note: Keeping in case we re-add the disclaimer
|
// 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::{
|
use crate::ui::{
|
||||||
fonts::IcedFonts as Fonts,
|
fonts::IcedFonts as Fonts,
|
||||||
ice::{
|
ice::{
|
||||||
@ -11,6 +11,7 @@ use crate::ui::{
|
|||||||
Element,
|
Element,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use i18n::{LanguageMetadata, Localization};
|
use i18n::{LanguageMetadata, Localization};
|
||||||
use iced::{
|
use iced::{
|
||||||
button, scrollable, text_input, Align, Button, Column, Container, Length, Row, Scrollable,
|
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;
|
const INPUT_TEXT_SIZE: u16 = 20;
|
||||||
|
|
||||||
/// Login screen for the main menu
|
/// Login screen for the main menu
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Screen {
|
pub struct Screen {
|
||||||
quit_button: button::State,
|
quit_button: button::State,
|
||||||
// settings_button: button::State,
|
// settings_button: button::State,
|
||||||
@ -36,21 +38,6 @@ pub struct Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub(super) fn view(
|
||||||
&mut self,
|
&mut self,
|
||||||
fonts: &Fonts,
|
fonts: &Fonts,
|
||||||
@ -59,7 +46,7 @@ impl Screen {
|
|||||||
login_info: &LoginInfo,
|
login_info: &LoginInfo,
|
||||||
error: Option<&str>,
|
error: Option<&str>,
|
||||||
i18n: &Localization,
|
i18n: &Localization,
|
||||||
is_selecting_language: bool,
|
show: &Showing,
|
||||||
selected_language_index: Option<usize>,
|
selected_language_index: Option<usize>,
|
||||||
language_metadatas: &[LanguageMetadata],
|
language_metadatas: &[LanguageMetadata],
|
||||||
button_style: style::button::Style,
|
button_style: style::button::Style,
|
||||||
@ -171,24 +158,25 @@ impl Screen {
|
|||||||
.height(Length::Units(180))
|
.height(Length::Units(180))
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.into()
|
.into()
|
||||||
} else if is_selecting_language {
|
|
||||||
self.language_selection.view(
|
|
||||||
fonts,
|
|
||||||
imgs,
|
|
||||||
i18n,
|
|
||||||
language_metadatas,
|
|
||||||
selected_language_index,
|
|
||||||
button_style,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
self.banner.view(
|
match show {
|
||||||
fonts,
|
Showing::Login => self.banner.view(
|
||||||
imgs,
|
fonts,
|
||||||
server_field_locked,
|
imgs,
|
||||||
login_info,
|
server_field_locked,
|
||||||
i18n,
|
login_info,
|
||||||
button_style,
|
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)
|
let central_column = Container::new(central_content)
|
||||||
@ -222,6 +210,7 @@ impl Screen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct LanguageSelectBanner {
|
pub struct LanguageSelectBanner {
|
||||||
okay_button: button::State,
|
okay_button: button::State,
|
||||||
language_buttons: Vec<button::State>,
|
language_buttons: Vec<button::State>,
|
||||||
@ -230,14 +219,6 @@ pub struct LanguageSelectBanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageSelectBanner {
|
impl LanguageSelectBanner {
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
okay_button: Default::default(),
|
|
||||||
language_buttons: Default::default(),
|
|
||||||
selection_list: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(
|
fn view(
|
||||||
&mut self,
|
&mut self,
|
||||||
fonts: &Fonts,
|
fonts: &Fonts,
|
||||||
@ -336,6 +317,7 @@ impl LanguageSelectBanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct LoginBanner {
|
pub struct LoginBanner {
|
||||||
pub username: text_input::State,
|
pub username: text_input::State,
|
||||||
pub password: text_input::State,
|
pub password: text_input::State,
|
||||||
@ -349,20 +331,6 @@ pub struct LoginBanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
fn view(
|
||||||
&mut self,
|
&mut self,
|
||||||
fonts: &Fonts,
|
fonts: &Fonts,
|
||||||
|
@ -4,6 +4,8 @@ mod connecting;
|
|||||||
mod credits;
|
mod credits;
|
||||||
mod login;
|
mod login;
|
||||||
mod servers;
|
mod servers;
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
mod world_selector;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
credits::Credits,
|
credits::Credits,
|
||||||
@ -53,6 +55,12 @@ image_ids_ice! {
|
|||||||
selection: "voxygen.element.ui.generic.frames.selection",
|
selection: "voxygen.element.ui.generic.frames.selection",
|
||||||
selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
|
selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
|
||||||
selection_press: "voxygen.element.ui.generic.frames.selection_press",
|
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: "voxygen.element.ui.generic.buttons.unlock",
|
||||||
unlock_hover: "voxygen.element.ui.generic.buttons.unlock_hover",
|
unlock_hover: "voxygen.element.ui.generic.buttons.unlock_hover",
|
||||||
unlock_press: "voxygen.element.ui.generic.buttons.unlock_press",
|
unlock_press: "voxygen.element.ui.generic.buttons.unlock_press",
|
||||||
@ -77,6 +85,47 @@ const BG_IMGS: [&str; 14] = [
|
|||||||
"voxygen.background.bg_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 {
|
pub enum Event {
|
||||||
LoginAttempt {
|
LoginAttempt {
|
||||||
username: String,
|
username: String,
|
||||||
@ -87,6 +136,10 @@ pub enum Event {
|
|||||||
ChangeLanguage(LanguageMetadata),
|
ChangeLanguage(LanguageMetadata),
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
StartSingleplayer,
|
StartSingleplayer,
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
InitSingleplayer,
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
SinglePlayerChange(WorldsChange),
|
||||||
Quit,
|
Quit,
|
||||||
// Note: Keeping in case we re-add the disclaimer
|
// Note: Keeping in case we re-add the disclaimer
|
||||||
//DisclaimerAccepted,
|
//DisclaimerAccepted,
|
||||||
@ -127,6 +180,26 @@ enum Screen {
|
|||||||
screen: connecting::Screen,
|
screen: connecting::Screen,
|
||||||
connection_state: ConnectionState,
|
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 {
|
struct Controls {
|
||||||
@ -147,7 +220,7 @@ struct Controls {
|
|||||||
selected_server_index: Option<usize>,
|
selected_server_index: Option<usize>,
|
||||||
login_info: LoginInfo,
|
login_info: LoginInfo,
|
||||||
|
|
||||||
is_selecting_language: bool,
|
show: Showing,
|
||||||
selected_language_index: Option<usize>,
|
selected_language_index: Option<usize>,
|
||||||
|
|
||||||
time: f64,
|
time: f64,
|
||||||
@ -163,6 +236,14 @@ enum Message {
|
|||||||
ShowCredits,
|
ShowCredits,
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
Singleplayer,
|
Singleplayer,
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
SingleplayerPlay,
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
WorldChanged(WorldsChange),
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
WorldCancelConfirmation,
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
WorldConfirmation(world_selector::Confirmation),
|
||||||
Multiplayer,
|
Multiplayer,
|
||||||
UnlockServerField,
|
UnlockServerField,
|
||||||
LanguageChanged(usize),
|
LanguageChanged(usize),
|
||||||
@ -202,7 +283,7 @@ impl Controls {
|
|||||||
}
|
}
|
||||||
} else { */
|
} else { */
|
||||||
Screen::Login {
|
Screen::Login {
|
||||||
screen: Box::new(login::Screen::new()),
|
screen: Box::default(),
|
||||||
error: None,
|
error: None,
|
||||||
};
|
};
|
||||||
//};
|
//};
|
||||||
@ -237,7 +318,7 @@ impl Controls {
|
|||||||
selected_server_index,
|
selected_server_index,
|
||||||
login_info,
|
login_info,
|
||||||
|
|
||||||
is_selecting_language: false,
|
show: Showing::Login,
|
||||||
selected_language_index,
|
selected_language_index,
|
||||||
|
|
||||||
time: 0.0,
|
time: 0.0,
|
||||||
@ -251,6 +332,7 @@ impl Controls {
|
|||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
key_layout: &Option<KeyLayout>,
|
key_layout: &Option<KeyLayout>,
|
||||||
dt: f32,
|
dt: f32,
|
||||||
|
#[cfg(feature = "singleplayer")] worlds: &crate::singleplayer::SingleplayerWorlds,
|
||||||
) -> Element<Message> {
|
) -> Element<Message> {
|
||||||
self.time += dt as f64;
|
self.time += dt as f64;
|
||||||
|
|
||||||
@ -306,7 +388,7 @@ impl Controls {
|
|||||||
&self.login_info,
|
&self.login_info,
|
||||||
error.as_deref(),
|
error.as_deref(),
|
||||||
&self.i18n.read(),
|
&self.i18n.read(),
|
||||||
self.is_selecting_language,
|
&self.show,
|
||||||
self.selected_language_index,
|
self.selected_language_index,
|
||||||
&language_metadatas,
|
&language_metadatas,
|
||||||
button_style,
|
button_style,
|
||||||
@ -334,6 +416,14 @@ impl Controls {
|
|||||||
&settings.controls,
|
&settings.controls,
|
||||||
key_layout,
|
key_layout,
|
||||||
),
|
),
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
Screen::WorldSelector { screen } => screen.view(
|
||||||
|
&self.fonts,
|
||||||
|
&self.imgs,
|
||||||
|
worlds,
|
||||||
|
&self.i18n.read(),
|
||||||
|
button_style,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
Container::new(
|
Container::new(
|
||||||
@ -360,7 +450,7 @@ impl Controls {
|
|||||||
Message::Quit => events.push(Event::Quit),
|
Message::Quit => events.push(Event::Quit),
|
||||||
Message::Back => {
|
Message::Back => {
|
||||||
self.screen = Screen::Login {
|
self.screen = Screen::Login {
|
||||||
screen: Box::new(login::Screen::new()),
|
screen: Box::default(),
|
||||||
error: None,
|
error: None,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -380,12 +470,52 @@ impl Controls {
|
|||||||
},
|
},
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
Message::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 {
|
self.screen = Screen::Connecting {
|
||||||
screen: connecting::Screen::new(ui),
|
screen: connecting::Screen::new(ui),
|
||||||
connection_state: ConnectionState::InProgress,
|
connection_state: ConnectionState::InProgress,
|
||||||
};
|
};
|
||||||
events.push(Event::StartSingleplayer);
|
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 => {
|
Message::Multiplayer => {
|
||||||
self.screen = Screen::Connecting {
|
self.screen = Screen::Connecting {
|
||||||
screen: connecting::Screen::new(ui),
|
screen: connecting::Screen::new(ui),
|
||||||
@ -403,7 +533,7 @@ impl Controls {
|
|||||||
Message::LanguageChanged(new_value) => {
|
Message::LanguageChanged(new_value) => {
|
||||||
events.push(Event::ChangeLanguage(language_metadatas.remove(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::Password(new_value) => self.login_info.password = new_value,
|
||||||
Message::Server(new_value) => {
|
Message::Server(new_value) => {
|
||||||
self.login_info.server = new_value;
|
self.login_info.server = new_value;
|
||||||
@ -451,7 +581,7 @@ impl Controls {
|
|||||||
if let Screen::Disclaimer { .. } = &self.screen {
|
if let Screen::Disclaimer { .. } = &self.screen {
|
||||||
events.push(Event::DisclaimerAccepted);
|
events.push(Event::DisclaimerAccepted);
|
||||||
self.screen = Screen::Login {
|
self.screen = Screen::Login {
|
||||||
screen: login::Screen::new(),
|
screen: login::Screen::default(),
|
||||||
error: None,
|
error: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -463,7 +593,7 @@ impl Controls {
|
|||||||
fn exit_connect_screen(&mut self) {
|
fn exit_connect_screen(&mut self) {
|
||||||
if matches!(&self.screen, Screen::Connecting { .. }) {
|
if matches!(&self.screen, Screen::Connecting { .. }) {
|
||||||
self.screen = Screen::Login {
|
self.screen = Screen::Login {
|
||||||
screen: Box::new(login::Screen::new()),
|
screen: Box::default(),
|
||||||
error: None,
|
error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -491,7 +621,7 @@ impl Controls {
|
|||||||
|| matches!(&self.screen, Screen::Login { .. })
|
|| matches!(&self.screen, Screen::Login { .. })
|
||||||
{
|
{
|
||||||
self.screen = Screen::Login {
|
self.screen = Screen::Login {
|
||||||
screen: Box::new(login::Screen::new()),
|
screen: Box::default(),
|
||||||
error: Some(error),
|
error: Some(error),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -626,11 +756,21 @@ impl MainMenuUi {
|
|||||||
pub fn maintain(&mut self, global_state: &mut GlobalState, dt: Duration) -> Vec<Event> {
|
pub fn maintain(&mut self, global_state: &mut GlobalState, dt: Duration) -> Vec<Event> {
|
||||||
let mut events = Vec::new();
|
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(
|
let (messages, _) = self.ui.maintain(
|
||||||
self.controls.view(
|
self.controls.view(
|
||||||
&global_state.settings,
|
&global_state.settings,
|
||||||
&global_state.window.key_layout,
|
&global_state.window.key_layout,
|
||||||
dt.as_secs_f32(),
|
dt.as_secs_f32(),
|
||||||
|
#[cfg(feature = "singleplayer")]
|
||||||
|
worlds,
|
||||||
),
|
),
|
||||||
global_state.window.renderer_mut(),
|
global_state.window.renderer_mut(),
|
||||||
None,
|
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
|
// Update the Discord activity on client initialization
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
let singleplayer = global_state.singleplayer.is_some();
|
let singleplayer = global_state.singleplayer.is_running();
|
||||||
#[cfg(not(feature = "singleplayer"))]
|
#[cfg(not(feature = "singleplayer"))]
|
||||||
let singleplayer = false;
|
let singleplayer = false;
|
||||||
|
|
||||||
|
@ -742,7 +742,7 @@ impl SettingsChange {
|
|||||||
global_state.discord = Discord::start(&global_state.tokio_runtime);
|
global_state.discord = Discord::start(&global_state.tokio_runtime);
|
||||||
|
|
||||||
#[cfg(feature = "singleplayer")]
|
#[cfg(feature = "singleplayer")]
|
||||||
let singleplayer = global_state.singleplayer.is_some();
|
let singleplayer = global_state.singleplayer.is_running();
|
||||||
#[cfg(not(feature = "singleplayer"))]
|
#[cfg(not(feature = "singleplayer"))]
|
||||||
let singleplayer = false;
|
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_export]
|
||||||
macro_rules! image_ids_ice {
|
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 struct $Ids {
|
||||||
$($( $v $name: $crate::ui::GraphicId, )*)*
|
$($( $(#[$($attribute)*])? $v $name: $crate::ui::GraphicId, )*)*
|
||||||
}
|
}
|
||||||
|
|
||||||
impl $Ids {
|
impl $Ids {
|
||||||
pub fn load(ui: &mut $crate::ui::ice::IcedUi) -> Result<Self, common::assets::Error> {
|
pub fn load(ui: &mut $crate::ui::ice::IcedUi) -> Result<Self, common::assets::Error> {
|
||||||
use $crate::ui::img_ids::GraphicCreator;
|
use $crate::ui::img_ids::GraphicCreator;
|
||||||
Ok(Self {
|
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,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
// use faster::*;
|
// use faster::*;
|
||||||
use itertools::izip;
|
use itertools::izip;
|
||||||
use noise::NoiseFn;
|
use noise::NoiseFn;
|
||||||
@ -2640,6 +2640,13 @@ pub fn do_erosion(
|
|||||||
|
|
||||||
(0..n_steps).for_each(|i| {
|
(0..n_steps).for_each(|i| {
|
||||||
debug!("Erosion iteration #{:?}", 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(
|
erode(
|
||||||
map_size_lg,
|
map_size_lg,
|
||||||
&mut h,
|
&mut h,
|
||||||
|
@ -43,6 +43,7 @@ use common::{
|
|||||||
calendar::Calendar,
|
calendar::Calendar,
|
||||||
grid::Grid,
|
grid::Grid,
|
||||||
lottery::Lottery,
|
lottery::Lottery,
|
||||||
|
resources::MapKind,
|
||||||
spiral::Spiral2d,
|
spiral::Spiral2d,
|
||||||
store::{Id, Store},
|
store::{Id, Store},
|
||||||
terrain::{
|
terrain::{
|
||||||
@ -71,7 +72,7 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, info, warn};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
/// Default base two logarithm of the world size, in chunks, per dimension.
|
/// 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)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct SizeOpts {
|
pub struct GenOpts {
|
||||||
x_lg: u32,
|
pub x_lg: u32,
|
||||||
y_lg: u32,
|
pub y_lg: u32,
|
||||||
scale: f64,
|
pub scale: f64,
|
||||||
|
pub map_kind: MapKind,
|
||||||
|
pub erosion_quality: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizeOpts {
|
impl Default for GenOpts {
|
||||||
pub fn new(x_lg: u32, y_lg: u32, scale: f64) -> Self { Self { x_lg, y_lg, scale } }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SizeOpts {
|
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
x_lg: 10,
|
x_lg: 10,
|
||||||
y_lg: 10,
|
y_lg: 10,
|
||||||
scale: 2.0,
|
scale: 2.0,
|
||||||
|
map_kind: MapKind::Square,
|
||||||
|
erosion_quality: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,17 +163,17 @@ impl Default for SizeOpts {
|
|||||||
pub enum FileOpts {
|
pub enum FileOpts {
|
||||||
/// If set, generate the world map and do not try to save to or load from
|
/// If set, generate the world map and do not try to save to or load from
|
||||||
/// file (default).
|
/// file (default).
|
||||||
Generate(SizeOpts),
|
Generate(GenOpts),
|
||||||
/// If set, generate the world map and save the world file (path is created
|
/// If set, generate the world map and save the world file (path is created
|
||||||
/// the same way screenshot paths are).
|
/// the same way screenshot paths are).
|
||||||
Save(SizeOpts),
|
Save(PathBuf, GenOpts),
|
||||||
/// Combination of Save and Load.
|
/// Combination of Save and Load.
|
||||||
/// Load map if exists or generate the world map and save the
|
/// Load map if exists or generate the world map and save the
|
||||||
/// world file.
|
/// world file.
|
||||||
LoadOrGenerate {
|
LoadOrGenerate {
|
||||||
name: String,
|
name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
opts: SizeOpts,
|
opts: GenOpts,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
},
|
},
|
||||||
@ -192,13 +193,15 @@ pub enum FileOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FileOpts {
|
impl Default for FileOpts {
|
||||||
fn default() -> Self { Self::Generate(SizeOpts::default()) }
|
fn default() -> Self { Self::Generate(GenOpts::default()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileOpts {
|
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 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 {
|
let map_size_lg = if let Some(map) = &parsed_world_file {
|
||||||
MapSizeLg::new(map.map_size_lg)
|
MapSizeLg::new(map.map_size_lg)
|
||||||
.expect("World size of loaded map does not satisfy invariants.")
|
.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
|
// FIXME: This is a hack! At some point we will have a more principled way of
|
||||||
// dealing with this.
|
// dealing with this.
|
||||||
let default_continent_scale_hack = 2.0/*4.0*/;
|
if let Some(map) = &parsed_world_file {
|
||||||
let continent_scale_hack = if let Some(map) = &parsed_world_file {
|
gen_opts.scale = map.continent_scale_hack;
|
||||||
map.continent_scale_hack
|
|
||||||
} else {
|
|
||||||
self.continent_scale_hack()
|
|
||||||
.unwrap_or(default_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
|
// TODO: this should return Option so that caller can choose fallback
|
||||||
fn map_size(&self) -> MapSizeLg {
|
fn map_size(&self) -> MapSizeLg {
|
||||||
match self {
|
match self {
|
||||||
Self::Generate(opts) | Self::Save(opts) | Self::LoadOrGenerate { opts, .. } => {
|
Self::Generate(opts) | Self::Save(_, opts) | Self::LoadOrGenerate { opts, .. } => {
|
||||||
MapSizeLg::new(Vec2 {
|
MapSizeLg::new(Vec2 {
|
||||||
x: opts.x_lg,
|
x: opts.x_lg,
|
||||||
y: opts.y_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
|
// TODO: This should probably return a Result, so that caller can choose
|
||||||
// whether to log error
|
// whether to log error
|
||||||
fn try_load_map(&self) -> Option<ModernMap> {
|
fn try_load_map(&self) -> Option<ModernMap> {
|
||||||
@ -357,7 +356,9 @@ impl FileOpts {
|
|||||||
// NOTE: we intentionally use pattern-matching here to get
|
// NOTE: we intentionally use pattern-matching here to get
|
||||||
// options, so that when gen opts get another field, compiler
|
// options, so that when gen opts get another field, compiler
|
||||||
// will force you to update following logic
|
// 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 {
|
let map = match map {
|
||||||
WorldFile::Veloren0_7_0(map) => map,
|
WorldFile::Veloren0_7_0(map) => map,
|
||||||
WorldFile::Veloren0_5_0(_) => {
|
WorldFile::Veloren0_5_0(_) => {
|
||||||
@ -403,25 +404,16 @@ impl FileOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn map_path(&self) -> Option<PathBuf> {
|
fn map_path(&self) -> Option<PathBuf> {
|
||||||
const MAP_DIR: &str = "./maps";
|
|
||||||
// TODO: Work out a nice bincode file extension.
|
// TODO: Work out a nice bincode file extension.
|
||||||
let file_name = match self {
|
match self {
|
||||||
Self::Save { .. } => {
|
Self::Save(path, _) => Some(PathBuf::from(&path)),
|
||||||
use std::time::SystemTime;
|
Self::LoadOrGenerate { name, .. } => {
|
||||||
|
const MAP_DIR: &str = "./maps";
|
||||||
Some(format!(
|
let file_name = format!("{}.bin", name);
|
||||||
"map_{}.bin",
|
Some(std::path::Path::new(MAP_DIR).join(file_name))
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.map(|d| d.as_millis())
|
|
||||||
.unwrap_or(0)
|
|
||||||
))
|
|
||||||
},
|
},
|
||||||
Self::LoadOrGenerate { name, .. } => Some(format!("{}.bin", name)),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
}
|
||||||
|
|
||||||
file_name.map(|name| std::path::Path::new(MAP_DIR).join(name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&self, map: &WorldFile) {
|
fn save(&self, map: &WorldFile) {
|
||||||
@ -452,6 +444,9 @@ impl FileOpts {
|
|||||||
if let Err(e) = bincode::serialize_into(writer, map) {
|
if let Err(e) = bincode::serialize_into(writer, map) {
|
||||||
warn!(?e, "Couldn't write 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;
|
let world_file = opts.world_file;
|
||||||
|
|
||||||
// Parse out the contents of various map formats into the values we need.
|
// 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
|
// Currently only used with LoadOrGenerate to know if we need to
|
||||||
// overwrite world file
|
// overwrite world file
|
||||||
let fresh = parsed_world_file.is_none();
|
let fresh = parsed_world_file.is_none();
|
||||||
|
|
||||||
let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
|
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
|
* 5_000.0f64
|
||||||
.div(32.0)
|
.div(32.0)
|
||||||
.mul(TerrainChunkSize::RECT_SIZE.x as f64);
|
.mul(TerrainChunkSize::RECT_SIZE.x as f64);
|
||||||
@ -688,6 +683,8 @@ impl WorldSim {
|
|||||||
let uplift_scale = 128.0;
|
let uplift_scale = 128.0;
|
||||||
let uplift_turb_scale = uplift_scale / 4.0;
|
let uplift_turb_scale = uplift_scale / 4.0;
|
||||||
|
|
||||||
|
info!("Starting world generation");
|
||||||
|
|
||||||
// NOTE: Changing order will significantly change WorldGen, so try not to!
|
// NOTE: Changing order will significantly change WorldGen, so try not to!
|
||||||
let gen_ctx = GenCtx {
|
let gen_ctx = GenCtx {
|
||||||
turb_x_nz: SuperSimplex::new().set_seed(rng.gen()),
|
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.
|
// Suppose the old world has grid spacing Δx' = Δy', new Δx = Δy.
|
||||||
// We define grid_scale such that Δx = height_scale * Δx' ⇒
|
// We define grid_scale such that Δx = height_scale * Δx' ⇒
|
||||||
// grid_scale = Δx / Δ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
|
// 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
|
// 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).
|
// grid (when a chunk isn't available).
|
||||||
let n_approx = 1.0;
|
let n_approx = 1.0;
|
||||||
let max_erosion_per_delta_t = 64.0 * delta_t_scale(n_approx);
|
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_small_steps = 0;
|
||||||
let n_post_load_steps = 0;
|
let n_post_load_steps = 0;
|
||||||
|
|
||||||
@ -821,18 +818,44 @@ impl WorldSim {
|
|||||||
let ((alt_base, _), (chaos, _)) = threadpool.join(
|
let ((alt_base, _), (chaos, _)) = threadpool.join(
|
||||||
|| {
|
|| {
|
||||||
uniform_noise(map_size_lg, |_, wposf| {
|
uniform_noise(map_size_lg, |_, wposf| {
|
||||||
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale (multiplied
|
match gen_opts.map_kind {
|
||||||
// value is from -0.35 * (CONFIG.mountain_scale * 1.05) to
|
MapKind::Square => {
|
||||||
// 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675 to
|
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale
|
||||||
// 0.3325).
|
// (multiplied value is from -0.35 *
|
||||||
Some(
|
// (CONFIG.mountain_scale * 1.05) to
|
||||||
(gen_ctx
|
// 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675
|
||||||
.alt_nz
|
// to 0.3325).
|
||||||
.get((wposf.div(10_000.0)).into_array())
|
Some(
|
||||||
.clamp(-1.0, 1.0))
|
(gen_ctx
|
||||||
.sub(0.05)
|
.alt_nz
|
||||||
.mul(0.35),
|
.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.
|
// Save map, if necessary.
|
||||||
// NOTE: We wll always save a map with latest version.
|
// NOTE: We wll always save a map with latest version.
|
||||||
let map = WorldFile::new(ModernMap {
|
let map = WorldFile::new(ModernMap {
|
||||||
continent_scale_hack,
|
continent_scale_hack: gen_opts.scale,
|
||||||
map_size_lg: map_size_lg.vec(),
|
map_size_lg: map_size_lg.vec(),
|
||||||
alt,
|
alt,
|
||||||
basement,
|
basement,
|
||||||
@ -1415,7 +1438,7 @@ impl WorldSim {
|
|||||||
|
|
||||||
let rivers = get_rivers(
|
let rivers = get_rivers(
|
||||||
map_size_lg,
|
map_size_lg,
|
||||||
continent_scale_hack,
|
gen_opts.scale,
|
||||||
&water_alt_pos,
|
&water_alt_pos,
|
||||||
&water_alt,
|
&water_alt,
|
||||||
&dh,
|
&dh,
|
||||||
|
Loading…
Reference in New Issue
Block a user