alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates client src menu mod.rs
mod layout;
mod text_field;
use std::time::{Duration, Instant};
use chrono::Datelike;
use common::JavaRandom;
use common::item::Item;
use common::stats;
use server::save::catalog::Entry;
use winit::keyboard::KeyCode;
use crate::assets::packs::Pack;
use crate::settings::{Binding, Options, Setting, key_code};
pub use layout::contains;
pub use text_field::TextField;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Page {
Title,
Worlds,
Create,
Rename,
Delete,
Options,
Video,
Controls,
Multiplayer,
Packs,
Pause,
Achievements,
Statistics,
Loading,
Saving,
Error,
Chat,
/// `GuiSleepMP`: chat while lying in bed on a server.
Sleep,
/// `GuiEditSign`.
Sign,
}
/// The sign being written by `GuiEditSign`.
pub struct SignEditor {
pub sign: crate::interaction::PlacedSign,
pub lines: [String; 4],
pub line: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
Open(Page),
Back,
Quit,
Play,
Create,
Rename,
Delete,
Connect,
Resume,
Leave,
Setting(Setting),
Bind(Binding),
Pack(usize),
OpenPackFolder,
SendChat,
FinishSign,
LeaveBed,
StatsTab(u8),
StatsSort(usize),
}
pub struct Menu {
pub page: Option<Page>,
pub worlds: Vec<Entry>,
pub selected: Option<usize>,
pub packs: Vec<Pack>,
pub fields: [TextField; 2],
pub folder: String,
pub splash: String,
splashes: Vec<String>,
splash_rand: JavaRandom,
pub message: String,
pub progress: f32,
pub statistics: crate::stats::Shared,
pub stats_tab: u8,
pub stats_sort: Option<(usize, bool)>,
pub achievement_scroll: [f32; 2],
achievement_drag: Option<[f32; 2]>,
pub loading_title: &'static str,
pub error_title: String,
pub scroll: f32,
pub binding: Option<Binding>,
pub remote: bool,
pub in_world: bool,
pub opened: Instant,
pub keyboard_focus: Option<usize>,
pub sign: Option<SignEditor>,
pub options_parent: Page,
pub error_parent: Page,
/// Whether the GPU supports descriptor heaps, so the Video Settings can switch to them.
pub descriptor_heaps: bool,
/// Whether the GPU and window system support frame pacing.
pub frame_pacing: bool,
/// The present modes the Video Settings cycle through.
pub present_modes: Vec<crate::gpu::PresentMode>,
slider: Option<(Setting, [f32; 4])>,
drag: Option<(f32, f32)>,
last_click: Option<(usize, Instant)>,
}
impl Menu {
/// A fresh `GuiMainMenu`: a random splash, unless a holiday has its own.
pub fn choose_splash(&mut self) {
let now = chrono::Local::now();
self.splash = match (now.month(), now.day()) {
(11, 9) => "Happy birthday, ez!".into(),
(6, 1) => "Happy birthday, Notch!".into(),
(12, 24) => "Merry X-mas!".into(),
(1, 1) => "Happy new year!".into(),
_ if self.splashes.is_empty() => "missingno".into(),
_ => {
let index = self.splash_rand.next_int(self.splashes.len() as i32);
self.splashes[index as usize].clone()
}
};
}
pub fn new(splashes: &str, seed: i64) -> Self {
let splashes = splashes.lines().map(str::trim).filter(|s| !s.is_empty());
let mut fields = [TextField::new("", 32), TextField::new("", 128)];
for field in &mut fields {
field.focused = false;
}
let mut menu = Self {
page: Some(Page::Title),
worlds: Vec::new(),
selected: None,
packs: Vec::new(),
fields,
folder: String::new(),
splash: String::new(),
splashes: splashes.map(str::to_owned).collect(),
splash_rand: JavaRandom::new(seed),
message: String::new(),
progress: 0.0,
statistics: Default::default(),
stats_tab: 0,
stats_sort: None,
achievement_scroll: [-82.0, -70.0],
achievement_drag: None,
loading_title: "Generating level",
error_title: String::new(),
scroll: 0.0,
binding: None,
remote: false,
in_world: false,
opened: Instant::now(),
keyboard_focus: None,
sign: None,
options_parent: Page::Title,
error_parent: Page::Title,
descriptor_heaps: false,
frame_pacing: false,
present_modes: Vec::new(),
slider: None,
drag: None,
last_click: None,
};
menu.choose_splash();
menu
}
pub fn open(&mut self, page: Page, options: &Options) {
self.page = Some(page);
self.scroll = 0.0;
self.achievement_drag = None;
self.slider = None;
self.drag = None;
self.last_click = None;
self.binding = None;
self.keyboard_focus = None;
self.opened = Instant::now();
self.fields
.iter_mut()
.for_each(|field| field.focused = false);
match page {
Page::Options if self.in_world => self.options_parent = Page::Pause,
Page::Options => self.options_parent = Page::Title,
Page::Statistics => (self.stats_tab, self.stats_sort) = (0, None),
Page::Achievements => self.achievement_scroll = [-82.0, -70.0],
Page::Create => {
self.fields = [
TextField::new("New World", 32),
TextField::new("", usize::MAX),
]
}
Page::Rename => {
if let Some(world) = self.selected_world() {
self.fields[0] = TextField::new(&world.name, 32);
}
}
Page::Multiplayer => self.fields[0] = TextField::new(&options.last_server, 128),
Page::Chat | Page::Sleep => self.fields[0] = TextField::chat(100),
_ => {}
}
self.fields[1].focused = false;
}
pub fn selected_world(&self) -> Option<&Entry> {
self.selected.and_then(|index| self.worlds.get(index))
}
pub fn fail(&mut self, title: &str, message: impl ToString, parent: Page, options: &Options) {
self.open(Page::Error, options);
self.error_title = title.into();
self.message = message.to_string();
self.error_parent = parent;
}
pub fn back(&self) -> Action {
match self.page {
Some(Page::Statistics) => Action::Open(Page::Pause),
Some(Page::Achievements) => Action::Resume,
Some(Page::Options) => Action::Open(self.options_parent),
Some(Page::Video | Page::Controls) => Action::Open(Page::Options),
Some(Page::Create | Page::Rename | Page::Delete) => Action::Open(Page::Worlds),
Some(Page::Pause | Page::Chat) => Action::Resume,
Some(Page::Sleep) => Action::LeaveBed,
Some(Page::Loading) => Action::Leave,
Some(Page::Error) => Action::Open(self.error_parent),
_ => Action::Open(Page::Title),
}
}
pub fn list_len(&self) -> usize {
match self.page {
Some(Page::Worlds) => self.worlds.len(),
Some(Page::Packs) => self.packs.len(),
Some(Page::Statistics) if self.stats_tab == 0 => stats::GENERAL.len(),
Some(Page::Statistics) => self.stat_items().len(),
_ => 0,
}
}
pub fn list_bottom(&self, size: [f32; 2]) -> f32 {
size[1]
- if self.page == Some(Page::Packs) {
51.0
} else {
64.0
}
}
/// The row height of the list, the height of its header and the distance a wheel step scrolls.
fn rows(&self) -> (f32, f32, f32) {
match (self.page, self.stats_tab) {
(Some(Page::Statistics), 0) => (10.0, 0.0, 5.0),
(Some(Page::Statistics), _) => (20.0, 20.0, 10.0),
_ => (36.0, 0.0, 18.0),
}
}
pub fn max_scroll(&self, size: [f32; 2]) -> f32 {
let (row, header, _) = self.rows();
(self.list_len() as f32 * row + header - (self.list_bottom(size) - 36.0)).max(0.0)
}
/// The height of the scroll bar's thumb in the world and pack lists.
pub fn scroll_thumb(&self, size: [f32; 2]) -> f32 {
let height = self.list_bottom(size) - 32.0;
(height * height / (self.list_len() as f32 * 36.0)).clamp(32.0, height - 8.0)
}
pub fn scroll(&mut self, delta: f32, size: [f32; 2]) {
let amount = self.rows().2;
self.scroll = (self.scroll - delta * amount).clamp(0.0, self.max_scroll(size));
}
pub fn mouse_move(&mut self, cursor: [f32; 2], size: [f32; 2], options: &mut Options) -> bool {
self.keyboard_focus = None;
if let Some(previous) = &mut self.achievement_drag {
self.achievement_scroll[0] =
(self.achievement_scroll[0] - (cursor[0] - previous[0])).clamp(-136.0, 114.0);
self.achievement_scroll[1] =
(self.achievement_scroll[1] - (cursor[1] - previous[1])).clamp(-232.0, 66.0);
*previous = cursor;
}
if let Some((setting, rect)) = self.slider {
setting.set_slider(options, (cursor[0] - rect[0] - 4.0) / (rect[2] - 8.0));
return true;
}
if let Some((y, multiplier)) = &mut self.drag {
self.scroll -= (cursor[1] - *y) * *multiplier;
*y = cursor[1];
self.scroll = self.scroll.clamp(0.0, self.max_scroll(size));
}
false
}
pub fn mouse_up(&mut self) -> bool {
self.drag = None;
self.achievement_drag = None;
self.slider.take().is_some()
}
pub fn click(
&mut self,
cursor: [f32; 2],
size: [f32; 2],
options: &mut Options,
) -> Option<Action> {
if self.binding.is_some() {
return None;
}
let map = [
(size[0] - 256.0) / 2.0 + 8.0,
(size[1] - 202.0) / 2.0 + 17.0,
224.0,
155.0,
];
if self.page == Some(Page::Achievements) && contains(map, cursor) {
self.achievement_drag = Some(cursor);
}
let in_list = (32.0..self.list_bottom(size)).contains(&cursor[1]);
if self.page == Some(Page::Statistics) && in_list {
let top = 36.0 - self.scroll;
if self.stats_tab != 0
&& (top..56.0 - self.scroll).contains(&cursor[1])
&& let Some(column) = (0..3).find(|&column| {
contains(
[size[0] / 2.0 - 31.0 + column as f32 * 50.0, top, 36.0, 20.0],
cursor,
)
})
{
return Some(Action::StatsSort(column));
}
self.drag = Some((cursor[1], 1.0));
}
for button in self.buttons(size, options) {
if button.enabled && contains(button.rect, cursor) {
if let Action::Setting(setting) = button.action
&& setting.slider(options).is_some()
{
self.slider = Some((setting, button.rect));
self.mouse_move(cursor, size, options);
}
return Some(button.action);
}
}
let fields = self.field_rects(size);
for (field, rect) in self.fields.iter_mut().zip(fields) {
field.focused = rect.is_some_and(|rect| contains(rect, cursor));
}
if matches!(self.page, Some(Page::Worlds | Page::Packs)) && in_list {
let max = self.max_scroll(size);
let multiplier = if (size[0] / 2.0 + 124.0..size[0] / 2.0 + 130.0).contains(&cursor[0])
&& max > 0.0
{
-max / (self.list_bottom(size) - 32.0 - self.scroll_thumb(size))
} else {
1.0
};
self.drag = Some((cursor[1], multiplier));
let y = cursor[1] - 36.0 + self.scroll;
let index = (y / 36.0) as usize;
if (size[0] / 2.0 - 110.0..size[0] / 2.0 + 110.0).contains(&cursor[0])
&& y >= 0.0
&& index < self.list_len()
{
if self.page == Some(Page::Packs) {
return Some(Action::Pack(index));
}
let double = self.last_click.is_some_and(|(old, time)| {
old == index && time.elapsed() < Duration::from_millis(250)
});
self.selected = Some(index);
self.last_click = Some((index, Instant::now()));
if double {
return Some(Action::Play);
}
}
}
None
}
pub fn key(
&mut self,
code: KeyCode,
text: Option<&str>,
control: bool,
shift: bool,
size: [f32; 2],
options: &mut Options,
) -> Option<Action> {
if self.page == Some(Page::Achievements) && options.matches(Binding::Inventory, code) {
return Some(Action::Resume);
}
// `GuiEditSign.keyTyped`, which leaves out escaping the screen
if self.page == Some(Page::Sign)
&& let Some(editor) = &mut self.sign
{
let line = &mut editor.lines[editor.line];
match code {
KeyCode::ArrowUp => editor.line = (editor.line + 3) & 3,
KeyCode::ArrowDown | KeyCode::Enter | KeyCode::NumpadEnter => {
editor.line = (editor.line + 1) & 3
}
KeyCode::Backspace => {
line.pop();
}
_ => {
for ch in text.unwrap_or_default().chars() {
if common::text::ALLOWED_CHARACTERS.contains(ch)
&& line.chars().count() < 15
{
line.push(ch);
}
}
}
}
return None;
}
if let Some(binding) = self.binding {
if let Some(key) = key_code(code) {
options.keys[binding as usize] = key;
self.binding = None;
}
return None;
}
if code == KeyCode::Escape {
// `GuiScreen.keyTyped` closes these settings screens and resumes the game
if self.in_world
&& matches!(
self.page,
Some(
Page::Options
| Page::Video
| Page::Controls
| Page::Statistics
| Page::Achievements
)
)
{
return Some(Action::Resume);
}
return (!matches!(self.page, Some(Page::Title | Page::Saving)))
.then_some(Action::Back);
}
if code == KeyCode::Tab {
let fields = self.field_rects(size);
if fields[0].is_some() {
if fields[1].is_some() {
let focused = self.fields[0].focused;
self.fields[0].focused = !focused;
self.fields[1].focused = focused;
} else {
self.fields[0].focused = true;
}
} else {
let buttons = self.buttons(size, options);
let indices: Vec<_> = (0..buttons.len()).filter(|&i| buttons[i].enabled).collect();
if !indices.is_empty() {
let len = indices.len();
let old = self
.keyboard_focus
.and_then(|focus| indices.iter().position(|index| *index == focus));
let next = match old {
None if shift => len - 1,
None => 0,
Some(old) if shift => (old + len - 1) % len,
Some(old) => (old + 1) % len,
};
self.keyboard_focus = Some(indices[next]);
}
}
return None;
}
if matches!(code, KeyCode::Enter | KeyCode::NumpadEnter) {
let buttons = self.buttons(size, options);
let index = self.keyboard_focus.unwrap_or(0);
return buttons
.get(index)
.filter(|button| button.enabled)
.map(|button| button.action);
}
if self.page == Some(Page::Worlds)
&& matches!(code, KeyCode::ArrowUp | KeyCode::ArrowDown)
&& !self.worlds.is_empty()
{
self.selected = Some(match self.selected {
None => 0,
Some(index) if code == KeyCode::ArrowUp => index.saturating_sub(1),
Some(index) => (index + 1).min(self.worlds.len() - 1),
});
let y = self.selected.unwrap() as f32 * 36.0;
self.scroll = self
.scroll
.min(y)
.max(y + 36.0 - (self.list_bottom(size) - 36.0))
.clamp(0.0, self.max_scroll(size));
}
for field in &mut self.fields {
if field.focused {
field.key(code, text, control, shift);
}
}
None
}
}
impl Menu {
/// The statistic bases of the columns of the blocks (1) or items (2) tab.
pub fn stat_columns(tab: u8) -> [i32; 3] {
if tab == 1 {
[stats::CRAFT, stats::USE, stats::MINE]
} else {
[stats::BREAK, stats::CRAFT, stats::USE]
}
}
pub fn stat_items(&self) -> Vec<Item> {
self.stat_items_for(self.stats_tab)
}
pub fn stat_items_for(&self, tab: u8) -> Vec<Item> {
let stats = self.statistics.lock().unwrap();
let columns = Self::stat_columns(tab);
let mut items: Vec<_> = (1..=2257)
.map(Item)
.filter(|&item| {
let id = i32::from(item.0);
item.properties().is_some()
&& if tab == 1 {
item.0 < 256 && id == stats::item_id(item)
} else {
item.0 >= 256
}
&& columns.iter().any(|base| stats.get(base + id) > 0)
})
.collect();
if let Some((column, descending)) = self.stats_sort {
let base = columns[column];
// `SorterStatsItem`: unavailable statistics stay last in either direction
let stat = |item: Item| {
let id = base + stats::item_id(item);
stats.guids.contains_key(&id).then(|| stats.get(id))
};
items.sort_unstable_by(|a, b| {
let order = match (stat(*a), stat(*b)) {
(Some(a), Some(b)) if descending => b.cmp(&a),
(Some(a), Some(b)) => a.cmp(&b),
(a, b) => a.is_none().cmp(&b.is_none()),
};
order.then_with(|| a.0.cmp(&b.0))
});
}
items
}
}