Map selector and generation UI

This commit is contained in:
Isse 2023-09-17 17:11:19 +00:00
parent b372a0c836
commit f4ca60cbb6
24 changed files with 1565 additions and 338 deletions

View File

@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Keybinds for zooming the camera (Defaults: ']' for zooming in and '[' for zooming out)
- Added the ability to make pets sit, they wont follow nor defend you in this state
- Portals that spawn in place of the last staircase at old style dungeons to prevent stair cheesing
- Mutliple singleplayer worlds and map generation UI.
### Changed

1
Cargo.lock generated
View File

@ -7231,6 +7231,7 @@ dependencies = [
"egui_wgpu_backend",
"egui_winit_platform",
"enum-iterator 1.4.1",
"enum-map",
"etagere",
"euc",
"gilrs",

View File

@ -32,6 +32,23 @@ main-login_process =
You can create an account over at
https://veloren.net/account/.
main-singleplayer-new = New
main-singleplayer-delete = Delete
main-singleplayer-regenerate = Regenerate
main-singleplayer-create_custom = Create Custom
main-singleplayer-invalid_name = Error: Invalid name
main-singleplayer-seed = Seed
main-singleplayer-random_seed = Random
main-singleplayer-size_lg = Logarithmic size
main-singleplayer-map_large_warning = Warning: Large worlds will take a long time to start for the first time
main-singleplayer-world_name = World name
main-singleplayer-map_scale = Vertical scaling
main-singleplayer-map_erosion_quality = Erosion quality
main-singleplayer-map_shape = Shape
main-singleplayer-play = Play
main-singleplayer-generate_and_play = Generate & Play
menu-singleplayer-confirm_delete = Are you sure you want to delete "{ $world_name }"
menu-singleplayer-confirm_regenerate = Are you sure you want to regenerate "{ $world_name }"
main-login-server_not_found = Server not found
main-login-authentication_error = Auth error on server
main-login-internal_error = Internal error on client (most likely, player character was deleted)

View File

@ -75,6 +75,24 @@ impl PlayerPhysicsSetting {
pub fn client_authoritative(&self) -> bool { !self.server_authoritative() }
}
/// Describe how the map should be generated.
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, enum_map::Enum)]
pub enum MapKind {
/// The normal square map, with oceans beyond the map edge
Square,
/// A more circular map, might have more islands
Circle,
}
impl std::fmt::Display for MapKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MapKind::Square => f.write_str("Square"),
MapKind::Circle => f.write_str("Circle"),
}
}
}
/// List of which players are using client-authoratative vs server-authoratative
/// physics, as a stop-gap until we can use server-authoratative physics for
/// everyone

View File

@ -55,7 +55,11 @@ impl Site {
.get(*faction)
.map_or(false, |f| f.good_or_evil == good_or_evil)
})
.min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos))
.min_by_key(|(faction_wpos, _)| {
faction_wpos
.as_::<i64>()
.distance_squared(wpos.as_::<i64>())
})
.map(|(_, faction)| *faction)
}),
population: Default::default(),

View File

@ -46,7 +46,7 @@ fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
// Only include sites in the list if they're not the current one and they're more populus
.filter(|(other_id, _, other_site2)| *other_id != site_id && other_site2.plots().len() > site2.plots().len())
.collect::<Vec<_>>();
other_sites.sort_by_key(|(_, other, _)| other.wpos.distance_squared(site.wpos) as i64);
other_sites.sort_by_key(|(_, other, _)| other.wpos.as_::<i64>().distance_squared(site.wpos.as_::<i64>()));
let mut max_size = 0;
// Remove sites that aren't in increasing order of size (Stalin sort?!)
other_sites.retain(|(_, _, other_site2)| {

View File

@ -131,8 +131,8 @@ use {
use crate::persistence::character_loader::CharacterScreenResponseKind;
use common::comp::Anchor;
#[cfg(feature = "worldgen")]
use world::{
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
pub use world::{
sim::{FileOpts, GenOpts, WorldOpts, DEFAULT_WORLD_MAP},
IndexOwned, World,
};

View File

@ -363,6 +363,7 @@ const MIGRATION_UPGRADE_GUARANTEE: &str = "Any valid file of an old verison shou
successfully migrate to the latest version.";
/// Combines all the editable settings into one struct that is stored in the ecs
#[derive(Clone)]
pub struct EditableSettings {
pub whitelist: Whitelist,
pub banlist: Banlist,

View File

@ -134,6 +134,7 @@ itertools = { workspace = true }
# Discord RPC
discord-sdk = { version = "0.3.0", optional = true }
enum-map = "2.5.0"
[target.'cfg(target_os = "macos")'.dependencies]
dispatch = "0.1.4"

View File

@ -42,6 +42,8 @@ pub mod window;
#[cfg(feature = "singleplayer")]
use crate::singleplayer::Singleplayer;
#[cfg(feature = "singleplayer")]
use crate::singleplayer::SingleplayerState;
#[cfg(feature = "egui-ui")]
use crate::ui::egui::EguiState;
use crate::{
@ -74,7 +76,7 @@ pub struct GlobalState {
pub info_message: Option<String>,
pub clock: Clock,
#[cfg(feature = "singleplayer")]
pub singleplayer: Option<Singleplayer>,
pub singleplayer: SingleplayerState,
// TODO: redo this so that the watcher doesn't have to exist for reloading to occur
pub i18n: LocalizationHandle,
pub clipboard: iced_winit::Clipboard,
@ -102,7 +104,7 @@ impl GlobalState {
#[cfg(feature = "singleplayer")]
pub fn paused(&self) -> bool {
self.singleplayer
.as_ref()
.as_running()
.map_or(false, Singleplayer::is_paused)
}
@ -110,10 +112,10 @@ impl GlobalState {
pub fn paused(&self) -> bool { false }
#[cfg(feature = "singleplayer")]
pub fn unpause(&self) { self.singleplayer.as_ref().map(|s| s.pause(false)); }
pub fn unpause(&self) { self.singleplayer.as_running().map(|s| s.pause(false)); }
#[cfg(feature = "singleplayer")]
pub fn pause(&self) { self.singleplayer.as_ref().map(|s| s.pause(true)); }
pub fn pause(&self) { self.singleplayer.as_running().map(|s| s.pause(true)); }
}
// TODO: appears to be currently unused by playstates

View File

@ -19,6 +19,8 @@ static GLOBAL: common_base::tracy_client::ProfiledAllocator<std::alloc::System>
common_base::tracy_client::ProfiledAllocator::new(std::alloc::System, 128);
use i18n::{self, LocalizationHandle};
#[cfg(feature = "singleplayer")]
use veloren_voxygen::singleplayer::SingleplayerState;
use veloren_voxygen::{
audio::AudioFrontend,
panic_handler,
@ -223,7 +225,7 @@ fn main() {
settings,
info_message: None,
#[cfg(feature = "singleplayer")]
singleplayer: None,
singleplayer: SingleplayerState::None,
i18n,
clipboard,
clear_shadows_next_frame: false,

View File

@ -1325,36 +1325,27 @@ impl Controls {
];
let right_column_content = if character_id.is_none() {
let site_slider = starter_slider(
i18n.get_msg("char_selection-starting_site").into_owned(),
30,
&mut sliders.starting_site,
self.possible_starting_sites.len() as u32 - 1,
*start_site_idx as u32,
|x| Message::StartingSite(x as usize),
imgs,
);
let map_sz = Vec2::new(500, 500);
let map_img = Image::new(self.map_img)
.height(Length::Units(map_sz.x))
.width(Length::Units(map_sz.y));
let site_name = Text::new(
self.possible_starting_sites[*start_site_idx]
.name
.as_deref()
.unwrap_or("Unknown")
)
.horizontal_alignment(HorizontalAlignment::Left)
.color(Color::from_rgb(131.0, 102.0, 0.0))
/* .stroke(Stroke {
color: Color::WHITE,
width: 1.0,
}) */;
}) */
//TODO: Add text-outline here whenever we updated iced to a version supporting
// this
let map = if let Some(info) = self.possible_starting_sites.get(*start_site_idx)
{
let site_name = Text::new(
self.possible_starting_sites[*start_site_idx]
.name
.as_deref()
.unwrap_or("Unknown"),
)
.horizontal_alignment(HorizontalAlignment::Left)
.color(Color::from_rgb(131.0, 102.0, 0.0));
let pos_frac = info
.wpos
.map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
@ -1388,6 +1379,15 @@ impl Controls {
if self.possible_starting_sites.is_empty() {
vec![map]
} else {
let site_slider = starter_slider(
i18n.get_msg("char_selection-starting_site").into_owned(),
30,
&mut sliders.starting_site,
self.possible_starting_sites.len() as u32 - 1,
*start_site_idx as u32,
|x| Message::StartingSite(x as usize),
imgs,
);
let site_buttons = Row::with_children(vec![
neat_button(
prev_starting_site_button,
@ -1972,9 +1972,9 @@ impl CharSelectionUi {
let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
#[cfg(feature = "singleplayer")]
let default_name = match global_state.singleplayer {
Some(_) => String::new(),
None => global_state.settings.networking.username.clone(),
let default_name = match global_state.singleplayer.is_running() {
true => String::new(),
false => global_state.settings.networking.username.clone(),
};
#[cfg(not(feature = "singleplayer"))]

View File

@ -4,7 +4,7 @@ mod ui;
use super::char_selection::CharSelectionState;
#[cfg(feature = "singleplayer")]
use crate::singleplayer::Singleplayer;
use crate::singleplayer::SingleplayerState;
use crate::{
render::{Drawer, GlobalsBindGroup},
settings::Settings,
@ -73,7 +73,7 @@ impl PlayState for MainMenuState {
// Reset singleplayer server if it was running already
#[cfg(feature = "singleplayer")]
{
global_state.singleplayer = None;
global_state.singleplayer = SingleplayerState::None;
}
// Updated localization in case the selected language was changed
@ -97,7 +97,7 @@ impl PlayState for MainMenuState {
// Poll server creation
#[cfg(feature = "singleplayer")]
{
if let Some(singleplayer) = &global_state.singleplayer {
if let Some(singleplayer) = global_state.singleplayer.as_running() {
match singleplayer.receiver.try_recv() {
Ok(Ok(())) => {
// Attempt login after the server is finished initializing
@ -113,7 +113,7 @@ impl PlayState for MainMenuState {
},
Ok(Err(e)) => {
error!(?e, "Could not start server");
global_state.singleplayer = None;
global_state.singleplayer = SingleplayerState::None;
self.init = InitState::None;
self.main_menu_ui.cancel_connection();
let server_err = match e {
@ -332,7 +332,7 @@ impl PlayState for MainMenuState {
// blocking TODO fix when the network rework happens
#[cfg(feature = "singleplayer")]
{
global_state.singleplayer = None;
global_state.singleplayer = SingleplayerState::None;
}
self.init = InitState::None;
self.main_menu_ui.cancel_connection();
@ -351,9 +351,32 @@ impl PlayState for MainMenuState {
},
#[cfg(feature = "singleplayer")]
MainMenuEvent::StartSingleplayer => {
let singleplayer = Singleplayer::new(&global_state.tokio_runtime);
global_state.singleplayer = Some(singleplayer);
global_state.singleplayer.run(&global_state.tokio_runtime);
},
#[cfg(feature = "singleplayer")]
MainMenuEvent::InitSingleplayer => {
global_state.singleplayer = SingleplayerState::init();
},
#[cfg(feature = "singleplayer")]
MainMenuEvent::SinglePlayerChange(change) => {
if let SingleplayerState::Init(ref mut init) = global_state.singleplayer {
match change {
ui::WorldsChange::SetActive(world) => init.current = world,
ui::WorldsChange::Delete(world) => init.remove(world),
ui::WorldsChange::Regenerate(world) => init.delete_map_file(world),
ui::WorldsChange::AddNew => init.new_world(),
ui::WorldsChange::CurrentWorldChange(change) => {
if let Some(world) = init
.current
.map(|i| &mut init.worlds[i])
.filter(|map| !map.is_generated)
{
change.apply(world);
init.save_current_meta();
}
},
}
}
},
MainMenuEvent::Quit => return PlayStateResult::Shutdown,
// Note: Keeping in case we re-add the disclaimer

View File

@ -1,4 +1,4 @@
use super::{Imgs, LoginInfo, Message, FILL_FRAC_ONE, FILL_FRAC_TWO};
use super::{Imgs, LoginInfo, Message, Showing, FILL_FRAC_ONE, FILL_FRAC_TWO};
use crate::ui::{
fonts::IcedFonts as Fonts,
ice::{
@ -11,6 +11,7 @@ use crate::ui::{
Element,
},
};
use i18n::{LanguageMetadata, Localization};
use iced::{
button, scrollable, text_input, Align, Button, Column, Container, Length, Row, Scrollable,
@ -22,6 +23,7 @@ const INPUT_WIDTH: u16 = 230;
const INPUT_TEXT_SIZE: u16 = 20;
/// Login screen for the main menu
#[derive(Default)]
pub struct Screen {
quit_button: button::State,
// settings_button: button::State,
@ -36,21 +38,6 @@ pub struct Screen {
}
impl Screen {
pub fn new() -> Self {
Self {
servers_button: Default::default(),
credits_button: Default::default(),
// settings_button: Default::default(),
quit_button: Default::default(),
language_select_button: Default::default(),
error_okay_button: Default::default(),
banner: LoginBanner::new(),
language_selection: LanguageSelectBanner::new(),
}
}
pub(super) fn view(
&mut self,
fonts: &Fonts,
@ -59,7 +46,7 @@ impl Screen {
login_info: &LoginInfo,
error: Option<&str>,
i18n: &Localization,
is_selecting_language: bool,
show: &Showing,
selected_language_index: Option<usize>,
language_metadatas: &[LanguageMetadata],
button_style: style::button::Style,
@ -171,24 +158,25 @@ impl Screen {
.height(Length::Units(180))
.padding(20)
.into()
} else if is_selecting_language {
self.language_selection.view(
fonts,
imgs,
i18n,
language_metadatas,
selected_language_index,
button_style,
)
} else {
self.banner.view(
fonts,
imgs,
server_field_locked,
login_info,
i18n,
button_style,
)
match show {
Showing::Login => self.banner.view(
fonts,
imgs,
server_field_locked,
login_info,
i18n,
button_style,
),
Showing::Languages => self.language_selection.view(
fonts,
imgs,
i18n,
language_metadatas,
selected_language_index,
button_style,
),
}
};
let central_column = Container::new(central_content)
@ -222,6 +210,7 @@ impl Screen {
}
}
#[derive(Default)]
pub struct LanguageSelectBanner {
okay_button: button::State,
language_buttons: Vec<button::State>,
@ -230,14 +219,6 @@ pub struct LanguageSelectBanner {
}
impl LanguageSelectBanner {
fn new() -> Self {
Self {
okay_button: Default::default(),
language_buttons: Default::default(),
selection_list: Default::default(),
}
}
fn view(
&mut self,
fonts: &Fonts,
@ -336,6 +317,7 @@ impl LanguageSelectBanner {
}
}
#[derive(Default)]
pub struct LoginBanner {
pub username: text_input::State,
pub password: text_input::State,
@ -349,20 +331,6 @@ pub struct LoginBanner {
}
impl LoginBanner {
fn new() -> Self {
Self {
username: Default::default(),
password: Default::default(),
server: Default::default(),
multiplayer_button: Default::default(),
#[cfg(feature = "singleplayer")]
singleplayer_button: Default::default(),
unlock_server_field_button: Default::default(),
}
}
fn view(
&mut self,
fonts: &Fonts,

View File

@ -4,6 +4,8 @@ mod connecting;
mod credits;
mod login;
mod servers;
#[cfg(feature = "singleplayer")]
mod world_selector;
use crate::{
credits::Credits,
@ -53,6 +55,12 @@ image_ids_ice! {
selection: "voxygen.element.ui.generic.frames.selection",
selection_hover: "voxygen.element.ui.generic.frames.selection_hover",
selection_press: "voxygen.element.ui.generic.frames.selection_press",
#[cfg(feature = "singleplayer")]
slider_range: "voxygen.element.ui.generic.slider.track",
#[cfg(feature = "singleplayer")]
slider_indicator: "voxygen.element.ui.generic.slider.indicator",
unlock: "voxygen.element.ui.generic.buttons.unlock",
unlock_hover: "voxygen.element.ui.generic.buttons.unlock_hover",
unlock_press: "voxygen.element.ui.generic.buttons.unlock_press",
@ -77,6 +85,47 @@ const BG_IMGS: [&str; 14] = [
"voxygen.background.bg_14",
];
#[cfg(feature = "singleplayer")]
#[derive(Clone)]
pub enum WorldChange {
Name(String),
Seed(u32),
SizeX(u32),
SizeY(u32),
Scale(f64),
MapKind(common::resources::MapKind),
ErosionQuality(f32),
DefaultGenOps,
}
#[cfg(feature = "singleplayer")]
impl WorldChange {
pub fn apply(self, world: &mut crate::singleplayer::SingleplayerWorld) {
let mut def = Default::default();
let gen_opts = world.gen_opts.as_mut().unwrap_or(&mut def);
match self {
WorldChange::Name(name) => world.name = name,
WorldChange::Seed(seed) => world.seed = seed,
WorldChange::SizeX(s) => gen_opts.x_lg = s,
WorldChange::SizeY(s) => gen_opts.y_lg = s,
WorldChange::Scale(scale) => gen_opts.scale = scale,
WorldChange::MapKind(kind) => gen_opts.map_kind = kind,
WorldChange::ErosionQuality(q) => gen_opts.erosion_quality = q,
WorldChange::DefaultGenOps => world.gen_opts = Some(Default::default()),
}
}
}
#[cfg(feature = "singleplayer")]
#[derive(Clone)]
pub enum WorldsChange {
SetActive(Option<usize>),
Delete(usize),
Regenerate(usize),
AddNew,
CurrentWorldChange(WorldChange),
}
pub enum Event {
LoginAttempt {
username: String,
@ -87,6 +136,10 @@ pub enum Event {
ChangeLanguage(LanguageMetadata),
#[cfg(feature = "singleplayer")]
StartSingleplayer,
#[cfg(feature = "singleplayer")]
InitSingleplayer,
#[cfg(feature = "singleplayer")]
SinglePlayerChange(WorldsChange),
Quit,
// Note: Keeping in case we re-add the disclaimer
//DisclaimerAccepted,
@ -127,6 +180,26 @@ enum Screen {
screen: connecting::Screen,
connection_state: ConnectionState,
},
#[cfg(feature = "singleplayer")]
WorldSelector {
screen: world_selector::Screen,
},
}
#[derive(PartialEq, Eq)]
enum Showing {
Login,
Languages,
}
impl Showing {
fn toggle(&mut self, other: Showing) {
if *self == other {
*self = Showing::Login;
} else {
*self = other;
}
}
}
struct Controls {
@ -147,7 +220,7 @@ struct Controls {
selected_server_index: Option<usize>,
login_info: LoginInfo,
is_selecting_language: bool,
show: Showing,
selected_language_index: Option<usize>,
time: f64,
@ -163,6 +236,14 @@ enum Message {
ShowCredits,
#[cfg(feature = "singleplayer")]
Singleplayer,
#[cfg(feature = "singleplayer")]
SingleplayerPlay,
#[cfg(feature = "singleplayer")]
WorldChanged(WorldsChange),
#[cfg(feature = "singleplayer")]
WorldCancelConfirmation,
#[cfg(feature = "singleplayer")]
WorldConfirmation(world_selector::Confirmation),
Multiplayer,
UnlockServerField,
LanguageChanged(usize),
@ -202,7 +283,7 @@ impl Controls {
}
} else { */
Screen::Login {
screen: Box::new(login::Screen::new()),
screen: Box::default(),
error: None,
};
//};
@ -237,7 +318,7 @@ impl Controls {
selected_server_index,
login_info,
is_selecting_language: false,
show: Showing::Login,
selected_language_index,
time: 0.0,
@ -251,6 +332,7 @@ impl Controls {
settings: &Settings,
key_layout: &Option<KeyLayout>,
dt: f32,
#[cfg(feature = "singleplayer")] worlds: &crate::singleplayer::SingleplayerWorlds,
) -> Element<Message> {
self.time += dt as f64;
@ -306,7 +388,7 @@ impl Controls {
&self.login_info,
error.as_deref(),
&self.i18n.read(),
self.is_selecting_language,
&self.show,
self.selected_language_index,
&language_metadatas,
button_style,
@ -334,6 +416,14 @@ impl Controls {
&settings.controls,
key_layout,
),
#[cfg(feature = "singleplayer")]
Screen::WorldSelector { screen } => screen.view(
&self.fonts,
&self.imgs,
worlds,
&self.i18n.read(),
button_style,
),
};
Container::new(
@ -360,7 +450,7 @@ impl Controls {
Message::Quit => events.push(Event::Quit),
Message::Back => {
self.screen = Screen::Login {
screen: Box::new(login::Screen::new()),
screen: Box::default(),
error: None,
};
},
@ -380,12 +470,52 @@ impl Controls {
},
#[cfg(feature = "singleplayer")]
Message::Singleplayer => {
self.screen = Screen::WorldSelector {
screen: world_selector::Screen::default(),
};
events.push(Event::InitSingleplayer);
},
#[cfg(feature = "singleplayer")]
Message::SingleplayerPlay => {
self.screen = Screen::Connecting {
screen: connecting::Screen::new(ui),
connection_state: ConnectionState::InProgress,
};
events.push(Event::StartSingleplayer);
},
#[cfg(feature = "singleplayer")]
Message::WorldChanged(change) => {
match change {
WorldsChange::Delete(_) | WorldsChange::Regenerate(_) => {
if let Screen::WorldSelector {
screen: world_selector::Screen { confirmation, .. },
} = &mut self.screen
{
*confirmation = None;
}
},
_ => {},
}
events.push(Event::SinglePlayerChange(change))
},
#[cfg(feature = "singleplayer")]
Message::WorldCancelConfirmation => {
if let Screen::WorldSelector {
screen: world_selector::Screen { confirmation, .. },
} = &mut self.screen
{
*confirmation = None;
}
},
#[cfg(feature = "singleplayer")]
Message::WorldConfirmation(new_confirmation) => {
if let Screen::WorldSelector {
screen: world_selector::Screen { confirmation, .. },
} = &mut self.screen
{
*confirmation = Some(new_confirmation);
}
},
Message::Multiplayer => {
self.screen = Screen::Connecting {
screen: connecting::Screen::new(ui),
@ -403,7 +533,7 @@ impl Controls {
Message::LanguageChanged(new_value) => {
events.push(Event::ChangeLanguage(language_metadatas.remove(new_value)));
},
Message::OpenLanguageMenu => self.is_selecting_language = !self.is_selecting_language,
Message::OpenLanguageMenu => self.show.toggle(Showing::Languages),
Message::Password(new_value) => self.login_info.password = new_value,
Message::Server(new_value) => {
self.login_info.server = new_value;
@ -451,7 +581,7 @@ impl Controls {
if let Screen::Disclaimer { .. } = &self.screen {
events.push(Event::DisclaimerAccepted);
self.screen = Screen::Login {
screen: login::Screen::new(),
screen: login::Screen::default(),
error: None,
};
}
@ -463,7 +593,7 @@ impl Controls {
fn exit_connect_screen(&mut self) {
if matches!(&self.screen, Screen::Connecting { .. }) {
self.screen = Screen::Login {
screen: Box::new(login::Screen::new()),
screen: Box::default(),
error: None,
}
}
@ -491,7 +621,7 @@ impl Controls {
|| matches!(&self.screen, Screen::Login { .. })
{
self.screen = Screen::Login {
screen: Box::new(login::Screen::new()),
screen: Box::default(),
error: Some(error),
}
} else {
@ -626,11 +756,21 @@ impl MainMenuUi {
pub fn maintain(&mut self, global_state: &mut GlobalState, dt: Duration) -> Vec<Event> {
let mut events = Vec::new();
#[cfg(feature = "singleplayer")]
let worlds_default = crate::singleplayer::SingleplayerWorlds::default();
#[cfg(feature = "singleplayer")]
let worlds = global_state
.singleplayer
.as_init()
.unwrap_or(&worlds_default);
let (messages, _) = self.ui.maintain(
self.controls.view(
&global_state.settings,
&global_state.window.key_layout,
dt.as_secs_f32(),
#[cfg(feature = "singleplayer")]
worlds,
),
global_state.window.renderer_mut(),
None,

View 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()
}
}
}

View File

@ -500,7 +500,7 @@ impl PlayState for SessionState {
{
// Update the Discord activity on client initialization
#[cfg(feature = "singleplayer")]
let singleplayer = global_state.singleplayer.is_some();
let singleplayer = global_state.singleplayer.is_running();
#[cfg(not(feature = "singleplayer"))]
let singleplayer = false;

View File

@ -742,7 +742,7 @@ impl SettingsChange {
global_state.discord = Discord::start(&global_state.tokio_runtime);
#[cfg(feature = "singleplayer")]
let singleplayer = global_state.singleplayer.is_some();
let singleplayer = global_state.singleplayer.is_running();
#[cfg(not(feature = "singleplayer"))]
let singleplayer = false;

View File

@ -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();
}
}

View 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();
}
}

View 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
},
})
}
}

View File

@ -166,17 +166,17 @@ macro_rules! image_ids {
}
#[macro_export]
macro_rules! image_ids_ice {
($($v:vis struct $Ids:ident { $( <$T:ty> $( $name:ident: $specifier:expr ),* $(,)? )* })*) => {
($($v:vis struct $Ids:ident { $( <$T:ty> $( $(#[$($attribute:tt)*])? $name:ident: $specifier:expr ),* $(,)? )* })*) => {
$(
$v struct $Ids {
$($( $v $name: $crate::ui::GraphicId, )*)*
$($( $(#[$($attribute)*])? $v $name: $crate::ui::GraphicId, )*)*
}
impl $Ids {
pub fn load(ui: &mut $crate::ui::ice::IcedUi) -> Result<Self, common::assets::Error> {
use $crate::ui::img_ids::GraphicCreator;
Ok(Self {
$($( $name: ui.add_graphic(<$T as GraphicCreator>::new_graphic($specifier)?), )*)*
$($( $(#[$($attribute)*])? $name: ui.add_graphic(<$T as GraphicCreator>::new_graphic($specifier)?), )*)*
})
}
}

View File

@ -7,7 +7,7 @@ use common::{
},
vol::RectVolSize,
};
use tracing::{debug, error, warn};
use tracing::{debug, error, info, warn};
// use faster::*;
use itertools::izip;
use noise::NoiseFn;
@ -2640,6 +2640,13 @@ pub fn do_erosion(
(0..n_steps).for_each(|i| {
debug!("Erosion iteration #{:?}", i);
// Print out the percentage complete. Do this at most 20 times.
if i % std::cmp::max(n_steps / 20, 1) == 0 {
let pct = (i as f64 / n_steps as f64) * 100.0;
info!("{:.2}% complete", pct);
}
erode(
map_size_lg,
&mut h,

View File

@ -43,6 +43,7 @@ use common::{
calendar::Calendar,
grid::Grid,
lottery::Lottery,
resources::MapKind,
spiral::Spiral2d,
store::{Id, Store},
terrain::{
@ -71,7 +72,7 @@ use std::{
sync::Arc,
};
use strum::IntoEnumIterator;
use tracing::{debug, warn};
use tracing::{debug, info, warn};
use vek::*;
/// Default base two logarithm of the world size, in chunks, per dimension.
@ -138,22 +139,22 @@ pub(crate) struct GenCtx {
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct SizeOpts {
x_lg: u32,
y_lg: u32,
scale: f64,
pub struct GenOpts {
pub x_lg: u32,
pub y_lg: u32,
pub scale: f64,
pub map_kind: MapKind,
pub erosion_quality: f32,
}
impl SizeOpts {
pub fn new(x_lg: u32, y_lg: u32, scale: f64) -> Self { Self { x_lg, y_lg, scale } }
}
impl Default for SizeOpts {
impl Default for GenOpts {
fn default() -> Self {
Self {
x_lg: 10,
y_lg: 10,
scale: 2.0,
map_kind: MapKind::Square,
erosion_quality: 1.0,
}
}
}
@ -162,17 +163,17 @@ impl Default for SizeOpts {
pub enum FileOpts {
/// If set, generate the world map and do not try to save to or load from
/// file (default).
Generate(SizeOpts),
Generate(GenOpts),
/// If set, generate the world map and save the world file (path is created
/// the same way screenshot paths are).
Save(SizeOpts),
Save(PathBuf, GenOpts),
/// Combination of Save and Load.
/// Load map if exists or generate the world map and save the
/// world file.
LoadOrGenerate {
name: String,
#[serde(default)]
opts: SizeOpts,
opts: GenOpts,
#[serde(default)]
overwrite: bool,
},
@ -192,13 +193,15 @@ pub enum FileOpts {
}
impl Default for FileOpts {
fn default() -> Self { Self::Generate(SizeOpts::default()) }
fn default() -> Self { Self::Generate(GenOpts::default()) }
}
impl FileOpts {
fn load_content(&self) -> (Option<ModernMap>, MapSizeLg, f64) {
fn load_content(&self) -> (Option<ModernMap>, MapSizeLg, GenOpts) {
let parsed_world_file = self.try_load_map();
let mut gen_opts = self.gen_opts().unwrap_or_default();
let map_size_lg = if let Some(map) = &parsed_world_file {
MapSizeLg::new(map.map_size_lg)
.expect("World size of loaded map does not satisfy invariants.")
@ -214,21 +217,26 @@ impl FileOpts {
//
// FIXME: This is a hack! At some point we will have a more principled way of
// dealing with this.
let default_continent_scale_hack = 2.0/*4.0*/;
let continent_scale_hack = if let Some(map) = &parsed_world_file {
map.continent_scale_hack
} else {
self.continent_scale_hack()
.unwrap_or(default_continent_scale_hack)
if let Some(map) = &parsed_world_file {
gen_opts.scale = map.continent_scale_hack;
};
(parsed_world_file, map_size_lg, continent_scale_hack)
(parsed_world_file, map_size_lg, gen_opts)
}
fn gen_opts(&self) -> Option<GenOpts> {
match self {
Self::Generate(opts) | Self::Save(_, opts) | Self::LoadOrGenerate { opts, .. } => {
Some(opts.clone())
},
_ => None,
}
}
// TODO: this should return Option so that caller can choose fallback
fn map_size(&self) -> MapSizeLg {
match self {
Self::Generate(opts) | Self::Save(opts) | Self::LoadOrGenerate { opts, .. } => {
Self::Generate(opts) | Self::Save(_, opts) | Self::LoadOrGenerate { opts, .. } => {
MapSizeLg::new(Vec2 {
x: opts.x_lg,
y: opts.y_lg,
@ -242,15 +250,6 @@ impl FileOpts {
}
}
fn continent_scale_hack(&self) -> Option<f64> {
match self {
Self::Generate(opts) | Self::Save(opts) | Self::LoadOrGenerate { opts, .. } => {
Some(opts.scale)
},
_ => None,
}
}
// TODO: This should probably return a Result, so that caller can choose
// whether to log error
fn try_load_map(&self) -> Option<ModernMap> {
@ -357,7 +356,9 @@ impl FileOpts {
// NOTE: we intentionally use pattern-matching here to get
// options, so that when gen opts get another field, compiler
// will force you to update following logic
let SizeOpts { x_lg, y_lg, scale } = opts;
let GenOpts {
x_lg, y_lg, scale, ..
} = opts;
let map = match map {
WorldFile::Veloren0_7_0(map) => map,
WorldFile::Veloren0_5_0(_) => {
@ -403,25 +404,16 @@ impl FileOpts {
}
fn map_path(&self) -> Option<PathBuf> {
const MAP_DIR: &str = "./maps";
// TODO: Work out a nice bincode file extension.
let file_name = match self {
Self::Save { .. } => {
use std::time::SystemTime;
Some(format!(
"map_{}.bin",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
))
match self {
Self::Save(path, _) => Some(PathBuf::from(&path)),
Self::LoadOrGenerate { name, .. } => {
const MAP_DIR: &str = "./maps";
let file_name = format!("{}.bin", name);
Some(std::path::Path::new(MAP_DIR).join(file_name))
},
Self::LoadOrGenerate { name, .. } => Some(format!("{}.bin", name)),
_ => None,
};
file_name.map(|name| std::path::Path::new(MAP_DIR).join(name))
}
}
fn save(&self, map: &WorldFile) {
@ -452,6 +444,9 @@ impl FileOpts {
if let Err(e) = bincode::serialize_into(writer, map) {
warn!(?e, "Couldn't write map");
}
if let Ok(p) = std::fs::canonicalize(path) {
info!("Map saved at {}", p.to_string_lossy());
}
}
}
@ -674,13 +669,13 @@ impl WorldSim {
let world_file = opts.world_file;
// Parse out the contents of various map formats into the values we need.
let (parsed_world_file, map_size_lg, continent_scale_hack) = world_file.load_content();
let (parsed_world_file, map_size_lg, gen_opts) = world_file.load_content();
// Currently only used with LoadOrGenerate to know if we need to
// overwrite world file
let fresh = parsed_world_file.is_none();
let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let continent_scale = continent_scale_hack
let continent_scale = gen_opts.scale
* 5_000.0f64
.div(32.0)
.mul(TerrainChunkSize::RECT_SIZE.x as f64);
@ -688,6 +683,8 @@ impl WorldSim {
let uplift_scale = 128.0;
let uplift_turb_scale = uplift_scale / 4.0;
info!("Starting world generation");
// NOTE: Changing order will significantly change WorldGen, so try not to!
let gen_ctx = GenCtx {
turb_x_nz: SuperSimplex::new().set_seed(rng.gen()),
@ -759,7 +756,7 @@ impl WorldSim {
// Suppose the old world has grid spacing Δx' = Δy', new Δx = Δy.
// We define grid_scale such that Δx = height_scale * Δx' ⇒
// grid_scale = Δx / Δx'.
let grid_scale = 1.0f64 / (4.0 / continent_scale_hack)/*1.0*/;
let grid_scale = 1.0f64 / (4.0 / gen_opts.scale)/*1.0*/;
// Now, suppose we want to generate a world with "similar" topography, defined
// in this case as having roughly equal slopes at steady state, with the
@ -801,7 +798,7 @@ impl WorldSim {
// grid (when a chunk isn't available).
let n_approx = 1.0;
let max_erosion_per_delta_t = 64.0 * delta_t_scale(n_approx);
let n_steps = 100;
let n_steps = (100.0 * gen_opts.erosion_quality) as usize;
let n_small_steps = 0;
let n_post_load_steps = 0;
@ -821,18 +818,44 @@ impl WorldSim {
let ((alt_base, _), (chaos, _)) = threadpool.join(
|| {
uniform_noise(map_size_lg, |_, wposf| {
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale (multiplied
// value is from -0.35 * (CONFIG.mountain_scale * 1.05) to
// 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675 to
// 0.3325).
Some(
(gen_ctx
.alt_nz
.get((wposf.div(10_000.0)).into_array())
.clamp(-1.0, 1.0))
.sub(0.05)
.mul(0.35),
)
match gen_opts.map_kind {
MapKind::Square => {
// "Base" of the chunk, to be multiplied by CONFIG.mountain_scale
// (multiplied value is from -0.35 *
// (CONFIG.mountain_scale * 1.05) to
// 0.35 * (CONFIG.mountain_scale * 0.95), but value here is from -0.3675
// to 0.3325).
Some(
(gen_ctx
.alt_nz
.get((wposf.div(10_000.0)).into_array())
.min(1.0)
.max(-1.0))
.sub(0.05)
.mul(0.35),
)
},
MapKind::Circle => {
let world_sizef = map_size_lg.chunks().map(|e| e as f64)
* TerrainChunkSize::RECT_SIZE.map(|e| e as f64);
Some(
(gen_ctx
.alt_nz
.get((wposf.div(5_000.0 * gen_opts.scale)).into_array())
.min(1.0)
.max(-1.0))
.add(
0.2 - ((wposf / world_sizef) * 2.0 - 1.0)
.magnitude_squared()
.powf(0.75)
.clamped(0.0, 1.0)
.powf(1.0)
* 0.6,
)
.mul(0.5),
)
},
}
})
},
|| {
@ -1285,7 +1308,7 @@ impl WorldSim {
// Save map, if necessary.
// NOTE: We wll always save a map with latest version.
let map = WorldFile::new(ModernMap {
continent_scale_hack,
continent_scale_hack: gen_opts.scale,
map_size_lg: map_size_lg.vec(),
alt,
basement,
@ -1415,7 +1438,7 @@ impl WorldSim {
let rivers = get_rivers(
map_size_lg,
continent_scale_hack,
gen_opts.scale,
&water_alt_pos,
&water_alt,
&dh,