// Copyright 2016 Matthew Collins // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::fs; use std::thread; use std::sync::mpsc; use std::rc::Rc; use std::cell::RefCell; use std::collections::HashMap; use crate::ui; use crate::render; use crate::format; use crate::format::{Component, TextComponent}; use crate::protocol; use serde_json; use std::time::{Duration}; use image; use base64; use rand; use rand::Rng; pub struct ServerList { elements: Option, disconnect_reason: Option, needs_reload: Rc>, server_protocol_versions: HashMap, } struct UIElements { logo: ui::logo::Logo, servers: Vec, _add_btn: ui::ButtonRef, _refresh_btn: ui::ButtonRef, _options_btn: ui::ButtonRef, _disclaimer: ui::TextRef, _disconnected: Option, } struct Server { back: ui::ImageRef, offset: f64, y: f64, motd: ui::FormattedRef, ping: ui::ImageRef, players: ui::TextRef, version: ui::FormattedRef, icon: ui::ImageRef, icon_texture: Option, done_ping: bool, recv: mpsc::Receiver, } struct PingInfo { address: String, motd: format::Component, ping: Duration, exists: bool, online: i32, max: i32, protocol_version: i32, protocol_name: String, favicon: Option, } impl Server { fn update_position(&mut self) { if self.offset < 0.0 { self.y = self.offset * 200.0; } else { self.y = self.offset * 100.0; } } } impl ServerList { pub fn new(disconnect_reason: Option) -> ServerList { ServerList { elements: None, disconnect_reason, needs_reload: Rc::new(RefCell::new(false)), server_protocol_versions: HashMap::new(), } } fn reload_server_list(&mut self, renderer: &mut render::Renderer, ui_container: &mut ui::Container) { let elements = self.elements.as_mut().unwrap(); *self.needs_reload.borrow_mut() = false; { // Clean up previous list icons. let mut tex = renderer.get_textures_ref().write().unwrap(); for server in &mut elements.servers { if let Some(ref icon) = server.icon_texture { tex.remove_dynamic(icon); } } } elements.servers.clear(); let file = match fs::File::open("servers.json") { Ok(val) => val, Err(_) => return, }; let servers_info: serde_json::Value = serde_json::from_reader(file).unwrap(); let servers = servers_info.get("servers").unwrap().as_array().unwrap(); let mut offset = 0.0; for (index, svr) in servers.iter().enumerate() { let name = svr.get("name").unwrap().as_str().unwrap().to_owned(); let address = svr.get("address").unwrap().as_str().unwrap().to_owned(); // Everything is attached to this let back = ui::ImageBuilder::new() .texture("steven:solid") .position(0.0, offset * 100.0) .size(700.0, 100.0) .colour((0, 0, 0, 100)) .alignment(ui::VAttach::Middle, ui::HAttach::Center) .create(ui_container); let (send, recv) = mpsc::channel::(); // Make whole entry interactable { let mut backr = back.borrow_mut(); let address = address.clone(); backr.add_hover_func(move |this, over, _| { this.colour.3 = if over { 200 } else { 100 }; false }); backr.add_click_func(move |_, game| { game.screen_sys.replace_screen(Box::new(super::connecting::Connecting::new(&address))); game.connect_to(&address); true }); } // Server name ui::TextBuilder::new() .text(name.clone()) .position(100.0, 5.0) .attach(&mut *back.borrow_mut()); // Server icon let icon = ui::ImageBuilder::new() .texture("misc/unknown_server") .position(5.0, 5.0) .size(90.0, 90.0) .attach(&mut *back.borrow_mut()); // Ping indicator let ping = ui::ImageBuilder::new() .texture("gui/icons") .position(5.0, 5.0) .size(20.0, 16.0) .texture_coords((0.0, 56.0 / 256.0, 10.0 / 256.0, 8.0 / 256.0)) .alignment(ui::VAttach::Top, ui::HAttach::Right) .attach(&mut *back.borrow_mut()); // Player count let players = ui::TextBuilder::new() .text("???") .position(30.0, 5.0) .alignment(ui::VAttach::Top, ui::HAttach::Right) .attach(&mut *back.borrow_mut()); // Server's message of the day let motd = ui::FormattedBuilder::new() .text(Component::Text(TextComponent::new("Connecting..."))) .position(100.0, 23.0) .max_width(700.0 - (90.0 + 10.0 + 5.0)) .attach(&mut *back.borrow_mut()); // Version information let version = ui::FormattedBuilder::new() .text(Component::Text(TextComponent::new(""))) .position(100.0, 5.0) .max_width(700.0 - (90.0 + 10.0 + 5.0)) .alignment(ui::VAttach::Bottom, ui::HAttach::Left) .attach(&mut *back.borrow_mut()); // Delete entry button let delete_entry = ui::ButtonBuilder::new() .position(0.0, 0.0) .size(25.0, 25.0) .alignment(ui::VAttach::Bottom, ui::HAttach::Right) .attach(&mut *back.borrow_mut()); { let mut btn = delete_entry.borrow_mut(); let txt = ui::TextBuilder::new() .text("X") .alignment(ui::VAttach::Middle, ui::HAttach::Center) .attach(&mut *btn); btn.add_text(txt); } // Edit entry button let edit_entry = ui::ButtonBuilder::new() .position(25.0, 0.0) .size(25.0, 25.0) .alignment(ui::VAttach::Bottom, ui::HAttach::Right) .attach(&mut *back.borrow_mut()); { let mut btn = edit_entry.borrow_mut(); let txt = ui::TextBuilder::new() .text("E") .alignment(ui::VAttach::Middle, ui::HAttach::Center) .attach(&mut *btn); btn.add_text(txt); let index = index; let sname = name.clone(); let saddr = address.clone(); btn.add_click_func(move |_, game| { game.screen_sys.replace_screen(Box::new(super::edit_server::EditServerEntry::new( Some((index, sname.clone(), saddr.clone())) ))); true }) } let mut server = Server { back, offset, y: 0.0, done_ping: false, recv, motd, ping, players, version, icon, icon_texture: None, }; server.update_position(); elements.servers.push(server); offset += 1.0; // Don't block the main thread whilst pinging the server thread::spawn(move || { match protocol::Conn::new(&address, protocol::SUPPORTED_PROTOCOLS[0]).and_then(|conn| conn.do_status()) { Ok(res) => { let mut desc = res.0.description; format::convert_legacy(&mut desc); let favicon = if let Some(icon) = res.0.favicon { let data_base64 = &icon["data:image/png;base64,".len()..]; let data = base64::decode(data_base64).unwrap(); Some(image::load_from_memory(&data).unwrap()) } else { None }; drop(send.send(PingInfo { address, motd: desc, ping: res.1, exists: true, online: res.0.players.online, max: res.0.players.max, protocol_version: res.0.version.protocol, protocol_name: res.0.version.name, favicon, })); } Err(err) => { let e = format!("{}", err); let mut msg = TextComponent::new(&e); msg.modifier.color = Some(format::Color::Red); let _ = send.send(PingInfo { address, motd: Component::Text(msg), ping: Duration::new(99999, 0), exists: false, online: 0, max: 0, protocol_version: 0, protocol_name: "".to_owned(), favicon: None, }); } } }); } } } impl super::Screen for ServerList { fn on_active(&mut self, renderer: &mut render::Renderer, ui_container: &mut ui::Container) { let logo = ui::logo::Logo::new(renderer.resources.clone(), ui_container); // Refresh the server list let refresh = ui::ButtonBuilder::new() .position(300.0, -50.0 - 15.0) .size(100.0, 30.0) .alignment(ui::VAttach::Middle, ui::HAttach::Center) .draw_index(2) .create(ui_container); { let mut refresh = refresh.borrow_mut(); let txt = ui::TextBuilder::new() .text("Refresh") .alignment(ui::VAttach::Middle, ui::HAttach::Center) .attach(&mut *refresh); refresh.add_text(txt); let nr = self.needs_reload.clone(); refresh.add_click_func(move |_, _| { *nr.borrow_mut() = true; true }) } // Add a new server to the list let add = ui::ButtonBuilder::new() .position(200.0, -50.0 - 15.0) .size(100.0, 30.0) .alignment(ui::VAttach::Middle, ui::HAttach::Center) .draw_index(2) .create(ui_container); { let mut add = add.borrow_mut(); let txt = ui::TextBuilder::new() .text("Add") .alignment(ui::VAttach::Middle, ui::HAttach::Center) .attach(&mut *add); add.add_text(txt); add.add_click_func(move |_, game| { game.screen_sys.replace_screen(Box::new(super::edit_server::EditServerEntry::new( None ))); true }) } // Options menu let options = ui::ButtonBuilder::new() .position(5.0, 25.0) .size(40.0, 40.0) .alignment(ui::VAttach::Bottom, ui::HAttach::Right) .create(ui_container); { let mut options = options.borrow_mut(); ui::ImageBuilder::new() .texture("steven:gui/cog") .position(0.0, 0.0) .size(40.0, 40.0) .alignment(ui::VAttach::Middle, ui::HAttach::Center) .attach(&mut *options); options.add_click_func(|_, game| { game.screen_sys.add_screen(Box::new(super::SettingsMenu::new(game.vars.clone(), false))); true }); } // Disclaimer let disclaimer = ui::TextBuilder::new() .text("Not affiliated with Mojang/Minecraft") .position(5.0, 5.0) .colour((255, 200, 200, 255)) .alignment(ui::VAttach::Bottom, ui::HAttach::Right) .create(ui_container); // If we are kicked from a server display the reason let disconnected = if let Some(ref disconnect_reason) = self.disconnect_reason { let (width, height) = ui::Formatted::compute_size(renderer, disconnect_reason, 600.0); let background = ui::ImageBuilder::new() .texture("steven:solid") .position(0.0, 3.0) .size(width.max(renderer.ui.size_of_string("Disconnected")) + 4.0, height + 4.0 + 16.0) .colour((0, 0, 0, 100)) .alignment(ui::VAttach::Top, ui::HAttach::Center) .draw_index(10) .create(ui_container); ui::TextBuilder::new() .text("Disconnected") .position(0.0, 2.0) .colour((255, 0, 0, 255)) .alignment(ui::VAttach::Top, ui::HAttach::Center) .attach(&mut *background.borrow_mut()); ui::FormattedBuilder::new() .text(disconnect_reason.clone()) .position(0.0, 18.0) .max_width(600.0) .alignment(ui::VAttach::Top, ui::HAttach::Center) .attach(&mut *background.borrow_mut()); Some(background) } else { None }; self.elements = Some(UIElements { logo, servers: Vec::new(), _add_btn: add, _refresh_btn: refresh, _options_btn: options, _disclaimer: disclaimer, _disconnected: disconnected, }); self.reload_server_list(renderer, ui_container); } fn on_deactive(&mut self, renderer: &mut render::Renderer, _ui_container: &mut ui::Container) { // Clean up { let elements = self.elements.as_mut().unwrap(); let mut tex = renderer.get_textures_ref().write().unwrap(); for server in &mut elements.servers { if let Some(ref icon) = server.icon_texture { tex.remove_dynamic(icon); } } } self.elements = None } fn tick(&mut self, delta: f64, renderer: &mut render::Renderer, ui_container: &mut ui::Container) -> Option> { if *self.needs_reload.borrow() { self.reload_server_list(renderer, ui_container); } let elements = self.elements.as_mut().unwrap(); elements.logo.tick(renderer); for s in &mut elements.servers { // Animate the entries { let mut back = s.back.borrow_mut(); let dy = s.y - back.y; if dy * dy > 1.0 { let y = back.y; back.y = y + delta * dy * 0.1; } else { back.y = s.y; } } // Keep checking to see if the server has finished being // pinged if !s.done_ping { match s.recv.try_recv() { Ok(res) => { s.done_ping = true; s.motd.borrow_mut().set_text(res.motd); // Selects the icon for the given ping range // TODO: switch to as_millis() experimental duration_as_u128 #50202 once available? let ping_ms = (res.ping.subsec_nanos() as f64)/1000000.0 + (res.ping.as_secs() as f64)*1000.0; let y = match ping_ms.round() as u64 { _x @ 0 ... 75 => 16.0 / 256.0, _x @ 76 ... 150 => 24.0 / 256.0, _x @ 151 ... 225 => 32.0 / 256.0, _x @ 226 ... 350 => 40.0 / 256.0, _x @ 351 ... 999 => 48.0 / 256.0, _ => 56.0 / 256.0, }; s.ping.borrow_mut().texture_coords.1 = y; if res.exists { { let mut players = s.players.borrow_mut(); let txt = if protocol::SUPPORTED_PROTOCOLS.contains(&res.protocol_version) { players.colour.1 = 255; players.colour.2 = 255; format!("{}/{}", res.online, res.max) } else { players.colour.1 = 85; players.colour.2 = 85; format!("Out of date {}/{}", res.online, res.max) }; players.text = txt; // TODO: store in memory instead of disk? but where? self.server_protocol_versions.insert(res.address, res.protocol_version); let mut out = fs::File::create("server_versions.json").unwrap(); serde_json::to_writer_pretty(&mut out, &self.server_protocol_versions).unwrap(); } let mut txt = TextComponent::new(&res.protocol_name); txt.modifier.color = Some(format::Color::Yellow); let mut msg = Component::Text(txt); format::convert_legacy(&mut msg); s.version.borrow_mut().set_text(msg); } if let Some(favicon) = res.favicon { let name: String = std::iter::repeat(()).map(|()| rand::thread_rng() .sample(&rand::distributions::Alphanumeric)) .take(30) .collect(); let tex = renderer.get_textures_ref(); s.icon_texture = Some(name.clone()); let icon_tex = tex.write() .unwrap() .put_dynamic(&name, favicon); s.icon.borrow_mut().texture = icon_tex.name; } } Err(mpsc::TryRecvError::Disconnected) => { s.done_ping = true; let mut txt = TextComponent::new("Channel dropped"); txt.modifier.color = Some(format::Color::Red); s.motd.borrow_mut().set_text(Component::Text(txt)); } _ => {} } } } None } fn on_scroll(&mut self, _: f64, y: f64) { let elements = self.elements.as_mut().unwrap(); if elements.servers.is_empty() { return; } let mut diff = y / 1.0; { let last = elements.servers.last().unwrap(); if last.offset + diff <= 2.0 { diff = 2.0 - last.offset; } let first = elements.servers.first().unwrap(); if first.offset + diff >= 0.0 { diff = -first.offset; } } for s in &mut elements.servers { s.offset += diff; s.update_position(); } } }