MCGalaxy/Levels/Level.cs

756 lines
30 KiB
C#

/*
Copyright 2010 MCSharp team (Modified for use with MCZall/MCLawl/MCGalaxy)
Dual-licensed under the Educational Community License, Version 2.0 and
the GNU General Public License, Version 3 (the "Licenses"); you may
not use this file except in compliance with the Licenses. You may
obtain a copy of the Licenses at
http://www.opensource.org/licenses/ecl2.php
http://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing,
software distributed under the Licenses are distributed on an "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the Licenses for the specific language governing
permissions and limitations under the Licenses.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using MCGalaxy.SQL;
using Timer = System.Timers.Timer;
using MCGalaxy.BlockPhysics;
using MCGalaxy.Levels.IO;
//WARNING! DO NOT CHANGE THE WAY THE LEVEL IS SAVED/LOADED!
//You MUST make it able to save and load as a new version other wise you will make old levels incompatible!
namespace MCGalaxy
{
public enum LevelPermission //int is default
{
Banned = -20,
Guest = 0,
Builder = 30,
AdvBuilder = 50,
Operator = 80,
Admin = 100,
Nobody = 120,
Null = 150
}
public sealed partial class Level : IDisposable
{
#region Delegates
public delegate void OnLevelLoad(string level);
public delegate void OnLevelLoaded(Level l);
public delegate void OnLevelSave(Level l);
public delegate void OnLevelUnload(Level l);
public delegate void OnPhysicsUpdate(ushort x, ushort y, ushort z, byte time, string extraInfo, Level l);
public delegate void OnPhysicsStateChanged(object sender, PhysicsState state);
#endregion
public static event OnPhysicsStateChanged PhysicsStateChanged;
public static bool cancelload;
public static bool cancelsave;
public static bool cancelphysics;
internal readonly FastList<Check> ListCheck = new FastList<Check>(); //A list of blocks that need to be updated
internal readonly FastList<Update> ListUpdate = new FastList<Update>(); //A list of block to change after calculation
internal readonly Dictionary<int, sbyte> leaves = new Dictionary<int, sbyte>();
// Holds block state for leaf decay
internal readonly Dictionary<int, bool[]> liquids = new Dictionary<int, bool[]>();
// Holds random flow data for liqiud physics
bool physicssate = false;
public bool Death;
public ExtrasCollection Extras = new ExtrasCollection();
public bool GrassDestroy = true;
public bool GrassGrow = true;
public bool Instant;
public bool Killer = true;
public List<UndoPos> UndoBuffer = new List<UndoPos>();
public List<Zone> ZoneList;
public bool ai = true;
public bool backedup;
public List<BlockPos> blockCache = new List<BlockPos>();
public bool Buildable = true, Deletable = true;
public byte weather;
public string terrainUrl = "", texturePackUrl = "";
public bool cancelsave1;
public bool cancelunload;
public bool changed;
public bool physicschanged
{
get { return ListCheck.Count > 0; }
}
public bool countdowninprogress;
public bool ctfmode;
public int currentUndo;
public ushort Width, Height, Length;
// NOTE: These are for legacy code only, you should use upper case Width/Height/Length
// as these correctly map Y to being Height
[Obsolete] public ushort width;
[Obsolete] public ushort height;
[Obsolete] public ushort depth;
[Obsolete] public ushort length;
public bool IsMuseum {
get { return name.StartsWith("&cMuseum " + Server.DefaultColor, StringComparison.Ordinal); }
}
public int drown = 70;
public bool edgeWater;
public int fall = 9;
public bool finite;
public bool fishstill;
public bool growTrees;
public bool guns = true;
public byte jailrotx, jailroty;
/// <summary> Color of the clouds (RGB packed into an int). Set to -1 to use client defaults. </summary>
public string CloudColor = null;
/// <summary> Color of the fog (RGB packed into an int). Set to -1 to use client defaults. </summary>
public string FogColor = null;
/// <summary> Color of the sky (RGB packed into an int). Set to -1 to use client defaults. </summary>
public string SkyColor = null;
/// <summary> Color of the blocks in shadows (RGB packed into an int). Set to -1 to use client defaults. </summary>
public string ShadowColor = null;
/// <summary> Color of the blocks in the light (RGB packed into an int). Set to -1 to use client defaults. </summary>
public string LightColor = null;
/// <summary> Elevation of the "ocean" that surrounds maps. Default is map height / 2. </summary>
public short EdgeLevel;
/// <summary> Elevation of the clouds. Default is map height + 2. </summary>
public short CloudsHeight;
/// <summary> Max fog distance the client can see. Default is 0, meaning use the client-side defined maximum fog distance. </summary>
public short MaxFogDistance;
/// <summary> The block which will be displayed on the horizon. </summary>
public byte HorizonBlock = Block.water;
/// <summary> The block which will be displayed on the edge of the map. </summary>
public byte EdgeBlock = Block.blackrock;
public BlockDefinition[] CustomBlockDefs;
public ushort jailx, jaily, jailz;
public int lastCheck;
public int lastUpdate;
public bool leafDecay;
public bool loadOnGoto = true;
public string motd = "ignore";
public string name;
public int overload = 1500;
public LevelPermission perbuildmax = LevelPermission.Nobody;
public LevelPermission permissionbuild = LevelPermission.Guest;
// What ranks can go to this map (excludes banned)
public LevelPermission permissionvisit = LevelPermission.Guest;
public LevelPermission pervisitmax = LevelPermission.Nobody;
public bool physPause;
public DateTime physResume;
public Thread physThread;
public Timer physTimer = new Timer(1000);
//public Timer physChecker = new Timer(1000);
public int physics
{
get { return Physicsint; }
set
{
if (value > 0 && Physicsint == 0)
StartPhysics();
Physicsint = value;
}
}
int Physicsint;
public bool randomFlow = true;
public bool realistic = true;
public byte rotx;
public byte roty;
public bool rp = true;
public ushort spawnx, spawny, spawnz;
public int speedPhysics = 250;
public string theme = "Normal";
public bool unload = true;
public bool worldChat = true;
public bool bufferblocks = Server.bufferblocks;
public List<BlockQueue.block> blockqueue = new List<BlockQueue.block>();
private readonly object physThreadLock = new object();
public List<C4.C4s> C4list = new List<C4.C4s>();
public Level(string n, ushort x, ushort y, ushort z, string type, int seed = 0, bool useSeed = false)
{
//onLevelSave += null;
Width = x;
Height = y;
Length = z;
if (Width < 16) Width = 16;
if (Height < 16) Height = 16;
if (Length < 16) Length = 16;
width = Width;
length = Height;
height = Length; depth = Length;
CustomBlockDefs = new BlockDefinition[256];
for (int i = 0; i < CustomBlockDefs.Length; i++)
CustomBlockDefs[i] = BlockDefinition.GlobalDefs[i];
name = n;
EdgeLevel = (short)(y / 2);
CloudsHeight = (short)(y + 2);
blocks = new byte[Width * Height * Length];
ChunksX = (Width + 15) >> 4;
ChunksY = (Height + 15) >> 4;
ChunksZ = (Length + 15) >> 4;
CustomBlocks = new byte[ChunksX * ChunksY * ChunksZ][];
ZoneList = new List<Zone>();
MapGen.Generate(this, type, seed, useSeed);
spawnx = (ushort)(Width / 2);
spawny = (ushort)(Height * 0.75f);
spawnz = (ushort)(Length / 2);
rotx = 0;
roty = 0;
//season = new SeasonsCore(this);
}
public List<Player> players
{
get { return getPlayers(); }
}
#region IDisposable Members
public void Dispose()
{
Extras.Clear();
liquids.Clear();
leaves.Clear();
ListCheck.Clear();
ListUpdate.Clear();
UndoBuffer.Clear();
blockCache.Clear();
ZoneList.Clear();
blockqueue.Clear();
blocks = null;
CustomBlocks = null;
}
#endregion
[Obsolete("Please use OnPhysicsUpdate.Register()")]
public event OnPhysicsUpdate PhysicsUpdate = null;
[Obsolete("Please use OnLevelUnloadEvent.Register()")]
public static event OnLevelUnload LevelUnload = null;
[Obsolete("Please use OnLevelSaveEvent.Register()")]
public static event OnLevelSave LevelSave = null;
//public static event OnLevelSave onLevelSave = null;
[Obsolete("Please use OnLevelUnloadEvent.Register()")]
public event OnLevelUnload onLevelUnload = null;
[Obsolete("Please use OnLevelUnloadEvent.Register()")]
public static event OnLevelLoad LevelLoad = null;
[Obsolete("Please use OnLevelUnloadEvent.Register()")]
public static event OnLevelLoaded LevelLoaded;
public bool Unload(bool silent = false, bool save = true)
{
if (Server.mainLevel == this) return false;
if (IsMuseum) return false;
if (Server.lava.active && Server.lava.map == this) return false;
if (LevelUnload != null)
LevelUnload(this);
OnLevelUnloadEvent.Call(this);
if (cancelunload)
{
Server.s.Log("Unload canceled by Plugin! (Map: " + name + ")");
cancelunload = false;
return false;
}
MovePlayersToMain();
if (changed && (!Server.ZombieModeOn || !Server.noLevelSaving))
{
if ((!Server.lava.active || !Server.lava.HasMap(name)) && save) Save(false, true);
saveChanges();
}
if (TntWarsGame.Find(this) != null)
{
foreach (TntWarsGame.player pl in TntWarsGame.Find(this).Players)
{
pl.p.CurrentTntGameNumber = -1;
Player.SendMessage(pl.p, "TNT Wars: The TNT Wars game you are currently playing has been deleted!");
pl.p.PlayingTntWars = false;
pl.p.canBuild = true;
TntWarsGame.SetTitlesAndColor(pl, true);
}
Server.s.Log("TNT Wars: Game deleted on " + name);
TntWarsGame.GameList.Remove(TntWarsGame.Find(this));
}
MovePlayersToMain();
Server.levels.Remove(this);
try
{
PlayerBot.RemoveAllFromLevel(this);
//physChecker.Stop();
//physChecker.Dispose();
physThread.Abort();
physThread.Join();
}
catch
{
}
finally
{
Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
if (!silent) Chat.GlobalMessageOps("&3" + name + Server.DefaultColor + " was unloaded.");
Server.s.Log(string.Format("{0} was unloaded.", name));
}
return true;
}
void MovePlayersToMain() {
PlayerInfo.players.ForEach(
p => {
if (p.level.name.ToLower() == name.ToLower()) {
Player.SendMessage(p, "You were moved to the main level as " + name + " was unloaded.");
Command.all.Find("goto").Use(p, Server.mainLevel.name);
}
}
);
}
public unsafe void saveChanges() {
if (blockCache.Count == 0) return;
List<BlockPos> tempCache = blockCache;
string date = new String('-', 19); //yyyy-mm-dd hh:mm:ss
using (DatabaseTransactionHelper transaction = DatabaseTransactionHelper.Create()) {
fixed (char* ptr = date) {
ptr[4] = '-'; ptr[7] = '-'; ptr[10] = ' '; ptr[13] = ':'; ptr[16] = ':';
DoSaveChanges(tempCache, ptr, date, transaction);
}
}
tempCache.Clear();
blockCache = new List<BlockPos>();
Server.s.Log("Saved BlockDB changes for:" + name);
}
unsafe bool DoSaveChanges(List<BlockPos> tempCache, char* ptr, string date,
DatabaseTransactionHelper transaction) {
string template = "INSERT INTO `Block" + name +
"` (Username, TimePerformed, X, Y, Z, type, deleted) VALUES (@Name, @Time, @X, @Y, @Z, @Tile, @Del)";
ushort x, y, z;
IDbCommand cmd = transaction.CreateCommand(template);
DbParameter nameP = transaction.CreateParam("@Name", DbType.AnsiStringFixedLength); cmd.Parameters.Add(nameP);
DbParameter timeP = transaction.CreateParam("@Time", DbType.AnsiStringFixedLength); cmd.Parameters.Add(timeP);
DbParameter xP = transaction.CreateParam("@X", DbType.UInt16); cmd.Parameters.Add(xP);
DbParameter yP = transaction.CreateParam("@Y", DbType.UInt16); cmd.Parameters.Add(yP);
DbParameter zP = transaction.CreateParam("@Z", DbType.UInt16); cmd.Parameters.Add(zP);
DbParameter tileP = transaction.CreateParam("@Tile", DbType.Byte); cmd.Parameters.Add(tileP);
DbParameter delP = transaction.CreateParam("@Del", DbType.Boolean); cmd.Parameters.Add(delP);
for (int i = 0; i < tempCache.Count; i++) {
BlockPos bP = tempCache[i];
IntToPos(bP.index, out x, out y, out z);
nameP.Value = bP.name;
DateTime time = Server.StartTimeLocal.AddSeconds(bP.timeDelta);
MakeInt(time.Year, 4, 0, ptr); MakeInt(time.Month, 2, 5, ptr); MakeInt(time.Day, 2, 8, ptr);
MakeInt(time.Hour, 2, 11, ptr); MakeInt(time.Minute, 2, 14, ptr); MakeInt(time.Second, 2, 17, ptr);
timeP.Value = date;
xP.Value = x; yP.Value = y; zP.Value = z;
tileP.Value = bP.type;
delP.Value = bP.deleted;
if (!DatabaseTransactionHelper.Execute(template, cmd)) {
cmd.Dispose();
transaction.Rollback(); return false;
}
}
cmd.Dispose();
transaction.Commit();
return true;
}
unsafe static void MakeInt(int value, int chars, int offset, char* ptr) {
for (int i = 0; i < chars; i++, value /= 10) {
char c = (char)('0' + (value % 10));
ptr[offset + (chars - 1 - i)] = c;
}
}
public bool InBound(ushort x, ushort y, ushort z)
{
return x >= 0 && y >= 0 && z >= 0 && x < Width && y < Height && z < Length;
}
[Obsolete]
public static Level Find(string name) { return LevelInfo.Find(name); }
[Obsolete]
public static Level FindExact(string name) { return LevelInfo.FindExact(name); }
public static void SaveSettings(Level level) {
LvlProperties.Save(level, "levels/level properties/" + level.name);
}
// Returns true if ListCheck does not already have an check in the position.
// Useful for fireworks, which depend on two physics blocks being checked, one with extraInfo.
public bool CheckClear(ushort x, ushort y, ushort z)
{
int b = PosToInt(x, y, z);
return !ListCheck.Exists(C => C.b == b);
}
public void Save(bool Override = false, bool clearPhysics = false)
{
//if (season.started)
// season.Stop(this);
if (blocks == null) return;
string path = "levels/" + name + ".lvl";
if (LevelSave != null)
LevelSave(this);
OnLevelSaveEvent.Call(this);
if (cancelsave1)
{
cancelsave1 = false;
return;
}
if (cancelsave)
{
cancelsave = false;
return;
}
try
{
if (!Directory.Exists("levels")) Directory.CreateDirectory("levels");
if (!Directory.Exists("levels/level properties")) Directory.CreateDirectory("levels/level properties");
if (changed || !File.Exists(path) || Override || (physicschanged && clearPhysics))
{
if (clearPhysics)
ClearPhysics();
if (File.Exists(path)) {
if (File.Exists(path + ".prev"))
File.Delete(path + ".prev");
File.Copy(path, path + ".prev");
File.Delete(path);
}
LvlFile.Save(this, path + ".backup");
File.Copy(path + ".backup", path);
SaveSettings(this);
Server.s.Log(string.Format("SAVED: Level \"{0}\". ({1}/{2}/{3})", name, players.Count,
PlayerInfo.players.Count, Server.players));
changed = false;
}
else
{
Server.s.Log("Skipping level save for " + name + ".");
}
}
catch (OutOfMemoryException e)
{
Server.ErrorLog(e);
if (Server.mono)
{
Process[] prs = Process.GetProcesses();
foreach (Process pr in prs)
{
if (pr.ProcessName == "MCGalaxy")
pr.Kill();
}
}
else
Command.all.Find("restart").Use(null, "");
}
catch (Exception e)
{
Server.s.Log("FAILED TO SAVE :" + name);
Player.GlobalMessage("FAILED TO SAVE :" + name);
Server.ErrorLog(e);
return;
}
//season.Start(this);
GC.Collect();
GC.WaitForPendingFinalizers();
}
public int Backup(bool Forced = false, string backupName = "")
{
if (!backedup || Forced)
{
int backupNumber = 1;
string backupPath = @Server.backupLocation;
if (Directory.Exists(string.Format("{0}/{1}", backupPath, name)))
{
backupNumber = Directory.GetDirectories(string.Format("{0}/" + name, backupPath)).Length + 1;
}
else
{
Directory.CreateDirectory(backupPath + "/" + name);
}
string path = string.Format("{0}/" + name + "/" + backupNumber, backupPath);
if (backupName != "")
{
path = string.Format("{0}/" + name + "/" + backupName, backupPath);
}
Directory.CreateDirectory(path);
string BackPath = string.Format("{0}/{1}.lvl", path, name);
string current = string.Format("levels/{0}.lvl", name);
try
{
File.Copy(current, BackPath, true);
backedup = true;
return backupNumber;
}
catch (Exception e)
{
Server.ErrorLog(e);
Server.s.Log(string.Format("FAILED TO INCREMENTAL BACKUP :{0}", name));
return -1;
}
}
Server.s.Log("Level unchanged, skipping backup");
return -1;
}
public static void CreateLeveldb(string givenName)
{
Database.executeQuery("CREATE TABLE if not exists `Block" + givenName +
"` (Username CHAR(20), TimePerformed DATETIME, X SMALLINT UNSIGNED, Y SMALLINT UNSIGNED, Z SMALLINT UNSIGNED, Type TINYINT UNSIGNED, Deleted " +
(Server.useMySQL ? "BOOL" : "INT") + ")");
Database.executeQuery("CREATE TABLE if not exists `Portals" + givenName +
"` (EntryX SMALLINT UNSIGNED, EntryY SMALLINT UNSIGNED, EntryZ SMALLINT UNSIGNED, ExitMap CHAR(20), ExitX SMALLINT UNSIGNED, ExitY SMALLINT UNSIGNED, ExitZ SMALLINT UNSIGNED)");
Database.executeQuery("CREATE TABLE if not exists `Messages" + givenName +
"` (X SMALLINT UNSIGNED, Y SMALLINT UNSIGNED, Z SMALLINT UNSIGNED, Message CHAR(255));");
Database.executeQuery("CREATE TABLE if not exists `Zone" + givenName +
"` (SmallX SMALLINT UNSIGNED, SmallY SMALLINT UNSIGNED, SmallZ SMALLINT UNSIGNED, BigX SMALLINT UNSIGNED, BigY SMALLINT UNSIGNED, BigZ SMALLINT UNSIGNED, Owner VARCHAR(20));");
}
public static Level Load(string givenName)
{
return Load(givenName, 0);
}
//givenName is safe against SQL injections, it gets checked in CmdLoad.cs
public static Level Load(string givenName, byte phys)
{
if (LevelLoad != null)
LevelLoad(givenName);
OnLevelLoadEvent.Call(givenName);
if (cancelload)
{
cancelload = false;
return null;
}
CreateLeveldb(givenName);
string path = string.Format("levels/{0}.lvl", givenName);
if (File.Exists(path))
{
try
{
Level level = LvlFile.Load(givenName, path);
level.permissionbuild = LevelPermission.Builder;
level.setPhysics(phys);
//level.textures = new LevelTextures(level);
level.backedup = true;
using (DataTable ZoneDB = Database.fillData("SELECT * FROM `Zone" + givenName + "`"))
{
Zone Zn;
for (int i = 0; i < ZoneDB.Rows.Count; ++i)
{
Zn.smallX = ushort.Parse(ZoneDB.Rows[i]["SmallX"].ToString());
Zn.smallY = ushort.Parse(ZoneDB.Rows[i]["SmallY"].ToString());
Zn.smallZ = ushort.Parse(ZoneDB.Rows[i]["SmallZ"].ToString());
Zn.bigX = ushort.Parse(ZoneDB.Rows[i]["BigX"].ToString());
Zn.bigY = ushort.Parse(ZoneDB.Rows[i]["BigY"].ToString());
Zn.bigZ = ushort.Parse(ZoneDB.Rows[i]["BigZ"].ToString());
Zn.Owner = ZoneDB.Rows[i]["Owner"].ToString();
level.ZoneList.Add(Zn);
}
}
level.jailx = (ushort)(level.spawnx * 32);
level.jaily = (ushort)(level.spawny * 32);
level.jailz = (ushort)(level.spawnz * 32);
level.jailrotx = level.rotx;
level.jailroty = level.roty;
level.StartPhysics();
//level.physChecker.Elapsed += delegate
//{
// if (!level.physicssate && level.physics > 0)
// level.StartPhysics();
//};
//level.physChecker.Start();
//level.season = new SeasonsCore(level);
try
{
DataTable foundDB = Database.fillData("SELECT * FROM `Portals" + givenName + "`");
for (int i = 0; i < foundDB.Rows.Count; ++i)
{
if (
!Block.portal(level.GetTile(ushort.Parse(foundDB.Rows[i]["EntryX"].ToString()),
ushort.Parse(foundDB.Rows[i]["EntryY"].ToString()),
ushort.Parse(foundDB.Rows[i]["EntryZ"].ToString()))))
{
Database.executeQuery("DELETE FROM `Portals" + givenName + "` WHERE EntryX=" +
foundDB.Rows[i]["EntryX"] + " AND EntryY=" +
foundDB.Rows[i]["EntryY"] + " AND EntryZ=" +
foundDB.Rows[i]["EntryZ"]);
}
}
foundDB = Database.fillData("SELECT * FROM `Messages" + givenName + "`");
for (int i = 0; i < foundDB.Rows.Count; ++i)
{
if (
!Block.mb(level.GetTile(ushort.Parse(foundDB.Rows[i]["X"].ToString()),
ushort.Parse(foundDB.Rows[i]["Y"].ToString()),
ushort.Parse(foundDB.Rows[i]["Z"].ToString()))))
{
//givenName is safe against SQL injections, it gets checked in CmdLoad.cs
Database.executeQuery("DELETE FROM `Messages" + givenName + "` WHERE X=" +
foundDB.Rows[i]["X"] + " AND Y=" + foundDB.Rows[i]["Y"] +
" AND Z=" + foundDB.Rows[i]["Z"]);
}
}
foundDB.Dispose();
} catch (Exception e) {
Server.ErrorLog(e);
}
try {
LvlProperties.Load(level, "levels/level properties/" + level.name);
} catch (Exception e) {
Server.ErrorLog(e);
}
BlockDefinition[] defs = BlockDefinition.Load(false, level);
for (int i = 0; i < defs.Length; i++) {
if (defs[i] == null) continue;
level.CustomBlockDefs[i] = defs[i];
}
Server.s.Log(string.Format("Level \"{0}\" loaded.", level.name));
if (LevelLoaded != null)
LevelLoaded(level);
OnLevelLoadedEvent.Call(level);
return level;
} catch (Exception ex) {
Server.ErrorLog(ex);
return null;
}
}
Server.s.Log("ERROR loading level.");
return null;
}
public static bool CheckLoadOnGoto(string givenName) {
string value = LevelInfo.FindOfflineProperty(givenName, "loadongoto");
if (value == null) return true;
bool load;
if (!bool.TryParse(value, out load)) return true;
return load;
}
public void ChatLevel(string message) { ChatLevel(message, LevelPermission.Banned); }
public void ChatLevelOps(string message) { ChatLevel(message, Server.opchatperm); }
public void ChatLevelAdmins(string message) { ChatLevel(message, Server.adminchatperm); }
public void ChatLevel(string message, LevelPermission minPerm) {
foreach (Player pl in PlayerInfo.players) {
if (pl.level != this) continue;
if (pl.group.Permission < minPerm) continue;
pl.SendMessage(message);
}
}
public void UpdateBlockPermissions() {
foreach (Player p in PlayerInfo.players) {
if (p.level != this) continue;
if (!p.HasCpeExt(CpeExt.BlockPermissions)) continue;
p.SendCurrentBlockPermissions();
}
}
public static LevelPermission PermissionFromName(string name)
{
Group foundGroup = Group.Find(name);
return foundGroup != null ? foundGroup.Permission : LevelPermission.Null;
}
public static string PermissionToName(LevelPermission perm)
{
Group foundGroup = Group.findPerm(perm);
return foundGroup != null ? foundGroup.name : ((int)perm).ToString();
}
public List<Player> getPlayers()
{
return PlayerInfo.players.Where(p => p.level == this).ToList();
}
public struct BlockPos {
public string name;
public int timeDelta;
public int index;
public byte type, extType;
public bool deleted;
}
public struct UndoPos {
public int location;
public byte newType, newExtType;
public byte oldType, oldExtType;
public DateTime timePerformed;
}
public struct Zone {
public string Owner;
public ushort bigX, bigY, bigZ;
public ushort smallX, smallY, smallZ;
}
}
}