{ Copyright (C) 2005 Remco Mulder This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA For source notes please refer to Notes.txt For license terms please refer to GPL.txt. These files should be stored in the root of the compression you received this source in. } // This unit controls all database access. unit DataBase; interface uses Core, Classes, SysUtils; const DATABASE_VERSION = 7; Day : array[1..7] of string = ('Sun', 'Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat'); type // Enumerated types TFighterType = (ftToll, ftDefensive, ftOffensive); TSectorExploredType = (etNo, etCalc, etDensity, etHolo); TProductType = (ptFuelOre, ptOrganics, ptEquipment); // Exceptions EDatabaseError = class(Exception); // database records TDataHeader = record ProgramName : string[12]; Version : Byte; Sectors, StarDock, Class0_1, Class0_2 : Word; Address, Description : string[40]; Port : Word; LoginScript : string[255]; Password, LoginName : string[40]; Game : Char; UseLogin : Boolean; RobFactor, StealFactor : Byte; LastPortCIM : TDateTime; end; PDataHeader = ^TDataHeader; TSpaceObject = record Quantity : LongInt; Owner : string[40]; FigType : TFighterType; end; PTrader = ^TTrader; TTrader = record Name, ShipType, ShipName : string[40]; Figs : LongInt; NextTrader : LongInt; end; PShip = ^TShip; TShip = record Name, Owner, ShipType : string[40]; Figs : LongInt; NextShip : LongInt; end; TPort = record Name : string[40]; Dead : Boolean; BuildTime, ClassIndex : Byte; BuyProduct : array[TProductType] of Boolean; ProductPercent : array[TProductType] of Byte; ProductAmount : array[TProductType] of Word; UpDate : TDateTime; end; PSector = ^TSector; TSector = record // Index : Word; Warp : array[1..6] of Word; SPort : TPort; NavHaz : Byte; Figs, Mines_Armid, Mines_Limpet : TSpaceObject; Constellation, Beacon : string[40]; UpDate : TDateTime; Anomoly : Boolean; Density : LongInt; Warps : Byte; Explored : TSectorExploredType; Ships, Traders, Planets, Vars : LongInt; end; PPlanet = ^TPlanet; TPlanet = record Name : string[40]; NextPlanet : LongInt; end; PWarpIn = ^TWarpIn; TWarpIn = record Origin : Word; NextWarpIn : PWarpIn; end; TSectorVar = record VarName: string[10]; Value: string[40]; NextVar: LongInt; end; PSectorVar = ^TSectorVar; TSectorItem = (itPlanet, itTrader, itShip); TEmptyRecordGroup = record RecordSize : Word; EmptyRecords : array of Integer; end; TModDatabase = class(TTWXModule, IModDatabase, ITWXGlobals) private CacheAllocated, FRecording, FUseCache, FDataBaseOpen : Boolean; FDataFilename : string; DataFile : File; DataCache, SectorWarpCache : Pointer; DBSize, CacheSize : Integer; EmptyRecordGroups : array of TEmptyRecordGroup; FDBHeader : TDataHeader; FProgramDir : string; procedure AddSectorVar(SectorIndex: Integer; SectorVar: PSectorVar); procedure FindSectorVar(SectorIndex: Integer; const VarName: string; var SectorVar: PSectorVar; var Index: Integer); procedure DeleteSectorVar(SectorIndex: Integer; const VarName: string); procedure AddWarpIn(Sect, Origin : Word); function GetEmptyRecord(Size : Integer) : Integer; procedure CacheEmptyRecords; procedure CacheEmptyRecord(Index : Integer; Size : Word); procedure ReadRecordList(List : TList; FirstPos : Integer); function WriteRecordList(List : TList; RecordSize : Word) : Integer; procedure PurgeRecordList(FirstPos : Integer); procedure WriteRecord(Rec : Pointer; Pos, Next : Integer; Size : Word); procedure ReadData(Data : Pointer; Index, Size : Integer); procedure WriteData(Data : Pointer; Index, Size : Integer); procedure OpenCache; procedure CloseCache; function GetLastPortCIM: TDateTime; procedure SetLastPortCIM(const Value: TDateTime); protected { ITWXGlobals } function GetProgramDir: string; procedure SetProgramDir(const Value: string); { IModDatabase } function GetDatabaseName: string; procedure SetDatabaseName(const Value: string); function GetUseCache: Boolean; procedure SetUseCache(Value: Boolean); function GetRecording: Boolean; procedure SetRecording(Value: Boolean); public procedure AfterConstruction; override; procedure BeforeDestruction; override; // DB control methods procedure OpenDataBase(Filename : string); procedure CloseDataBase; procedure CreateDatabase(Filename : string; Head : TDataHeader); // main storage/retrieval methods procedure SaveSector(S: TSector; Index: Integer; ShipList: TList = nil; TraderList: TList = nil; PlanetList: TList = nil); function LoadSector(I : Integer) : TSector; procedure UpdateWarps(SectIndex : Integer); function GetSectorItems(ItemType : TSectorItem; Sector : TSector) : TList; function GetWarpsIn(Sect : Integer) : TList; function GetBackDoors(S: TSector; SectorIndex: Integer) : TList; function PlotWarpCourse(FromSect, ToSect : Word) : TList; procedure WriteHeader; procedure DumpData; procedure SetSectorVar(SectorIndex: Integer; const VarName, VarValue: string); function GetSectorVar(SectorIndex: Integer; const VarName: string): string; // null item retrieval procedure NULLSector(var Sector : TSector); procedure NULLPlanet(var Planet : TPlanet); procedure NULLTrader(var Trader : TTrader); procedure NULLShip(var Ship : TShip); property Sectors[Index : Integer] : TSector read LoadSector; property DataBaseOpen : Boolean read FDataBaseOpen; property DBHeader: TDataHeader read FDBHeader; property LastPortCIM: TDateTime read GetLastPortCIM write SetLastPortCIM; published property DatabaseName: string read GetDatabaseName write SetDatabaseName; property UseCache: Boolean read GetUseCache write SetUseCache; property Recording: Boolean read GetRecording write SetRecording; end; function GetBlankHeader : PDataHeader; implementation uses Global, Utility, Windows, Forms, Ansi, Dialogs; type TWarpItem = class(TObject) public Index : Word; Parent : TWarpItem; end; function WarpsTo(Source: TSector; DestIndex: Integer) : Boolean; begin WarpsTo := (Source.Warp[1] = DestIndex) or (Source.Warp[2] = DestIndex) or (Source.Warp[3] = DestIndex) or (Source.Warp[4] = DestIndex) or (Source.Warp[5] = DestIndex) or (Source.Warp[6] = DestIndex); end; function GetBlankHeader : PDataHeader; begin Result := AllocMem(SizeOf(TDataHeader)); ZeroMemory(Result, SizeOf(TDataHeader)); Result^.ProgramName := 'TWX DATABASE'; Result^.Version := DATABASE_VERSION; Result^.Address := ''; Result^.Port := 23; end; // *********************************************************** // Public Implementation procedure TModDatabase.AfterConstruction; begin inherited; // initialise variables FDatabaseOpen := FALSE; SectorWarpCache := nil; FUseCache := True; FRecording := True; end; procedure TModDatabase.BeforeDestruction; begin // ensure database is closed CloseDataBase; inherited; end; function TModDatabase.GetProgramDir: string; begin Result := FProgramDir; end; procedure TModDatabase.SetProgramDir(const Value: string); begin FProgramDir := Value; end; // --------------------------- // DB control methods procedure TModDatabase.OpenDataBase(Filename : string); var WarpCount, I, X : Integer; S : TSector; begin if (DataBaseOpen) then Exit; TWXServer.Broadcast(endl + ANSI_15 + 'Loading database: ' + ANSI_7 + Filename + endl); FDataFilename := Filename; SetCurrentDir(FProgramDir); // Open database AssignFile(DataFile, Filename); {$I-} Reset(DataFile, 1); if (IOResult <> 0) then begin TWXServer.Broadcast(endl + ANSI_12 + 'Warning: This database does not exist. No data will be saved/retrieved' + ANSI_7 + endl); CloseDatabase; Exit; end; {$I+} // Check database validity and version number Seek(DataFile, 0); BlockRead(DataFile, FDBHeader, SizeOf(TDataHeader)); if (DBHeader.ProgramName <> 'TWX DATABASE') then begin TWXServer.Broadcast(endl + ANSI_12 + 'Warning: This database has been corrupted, no data will be saved/retrieved' + ANSI_7 + endl); CloseDatabase; Exit; end; if (DBHeader.Version <> DATABASE_VERSION) then begin TWXServer.Broadcast(endl + ANSI_12 + 'Warning: Database version ' + IntToStr(DBHeader.Version) + ', expected version ' + IntToStr(DATABASE_VERSION) + ', no data will be saved/retrieved' + ANSI_7 + endl); CloseDatabase; Exit; end; if (UseCache) then OpenCache else CloseCache; FDatabaseOpen := TRUE; DBSize := FileSize(DataFile); // cache empty record indexes CacheEmptyRecords; // construct sector warp cache SectorWarpCache := AllocMem(DBHeader.Sectors * 4); WarpCount := 0; for I := 1 to DBHeader.Sectors do begin S := LoadSector(I); for X := 1 to 6 do if (S.Warp[X] > 0) then begin Inc(WarpCount); AddWarpIn(S.Warp[X], I); end else Break; end; TWXLog.DatabaseChanged; TWXServer.Broadcast(endl + ANSI_15 + 'Database successfully loaded - ' + IntToStr(DBHeader.Sectors) + ' sectors, ' + IntToStr(WarpCount) + ' warps' + endl); TWXGUI.DatabaseName := StripFileExtension(ShortFilename(Filename)); end; procedure TModDatabase.CloseDataBase; var W, Last : PWarpIn; P : Pointer; I, CacheEnd : Integer; begin if not (FDatabaseOpen) then Exit; {$I-} CloseFile(DataFile); {$I+} FDatabaseOpen := FALSE; // deconstruct sector warp cache P := SectorWarpCache; CacheEnd := Integer(P) + DBHeader.Sectors * 4; while (Integer(P) < CacheEnd) do begin W := PWarpIn(P^); while (W <> nil) do begin Last := W; W := W^.NextWarpIn; FreeMem(Last); end; P := Pointer(Integer(P) + SizeOf(Pointer)); end; // purge old empty record caches if (Length(EmptyRecordGroups) > 0) then for I := 0 to Length(EmptyRecordGroups) - 1 do SetLength(EmptyRecordGroups[I].EmptyRecords, 0); SetLength(EmptyRecordGroups, 0); FreeMem(SectorWarpCache); CloseCache; FDataFilename := ''; end; procedure TModDatabase.CreateDatabase(Filename : string; Head : TDataHeader); var I : Integer; Sect : TSector; F : File; FileOpen : Boolean; begin // Make a database - it doesn't exist TWXServer.Broadcast(endl + ANSI_15 + 'Creating database: ' + ANSI_7 + Filename + ANSI_15 + ' (' + IntToStr(Head.Sectors) + ')' + ANSI_7 + endl); SetCurrentDir(FProgramDir); FileOpen := FALSE; try AssignFile(F, Filename); ReWrite(F, 1); FileOpen := TRUE; BlockWrite(F, Head, SizeOf(TDataHeader)); NULLSector(Sect); for I := 0 to Head.Sectors do BlockWrite(F, Sect, SizeOf(Sect)); finally if (FileOpen) then CloseFile(F); end; end; // --------------------------- // main storage/retrieval methods procedure TModDatabase.SaveSector(S: TSector; Index: Integer; ShipList: TList = nil; TraderList: TList = nil; PlanetList: TList = nil); var Sect : TSector; I, X : Integer; Bad, Found : Boolean; WarpsIn : TList; Product : TProductType; begin // Save this sector to file if set to record data if (Recording) and (DatabaseOpen) then begin Bad := (Index < 1) or (Index > DBHeader.Sectors); I := 1; while not Bad and (I <= 6) do begin if (S.Warp[I] > DBHeader.Sectors) then Bad := True; Inc(I); end; if Bad then begin SetForegroundWindow(Application.Handle); TWXServer.ClientMessage('Unable to store sector ''' + IntToStr(Index) + ''', closing database.'); CloseDatabase; Exit; end; ReadData(@Sect, Index * SizeOf(TSector) + SizeOf(TDataHeader), SizeOf(Sect)); // If this sector has been probed, recall warps if seen before if (S.Warp[1] = 0) and (Sect.Warp[1] <> 0) then S.Warp := Sect.Warp; // Don't go over density or anomoly readings unless this is a density scan if (S.Density = -1) then begin S.Density := Sect.Density; S.Anomoly := Sect.Anomoly; end; // Don't go over port details unless they are specified if (S.SPort.UpDate = 0) then begin S.SPort.Update := Sect.SPort.UpDate; for Product := Low(TProductType) to High(TProductType) do begin S.SPort.ProductAmount[Product] := Sect.SPort.ProductAmount[Product]; S.SPort.ProductPercent[Product] := Sect.SPort.ProductPercent[Product]; end; end; // save stardock details if this sector has it if (S.SPort.ClassIndex = 9) and (Sect.SPort.ClassIndex <> 9) then begin FDBHeader.StarDock := Index; WriteHeader; end; // update sector warp count S.Warps := 6; for I := 1 to 6 do if (S.Warp[I] = 0) then begin S.Warps := I - 1; Break; end; // Purge old ship, trader and planet data PurgeRecordList(Sect.Ships); PurgeRecordList(Sect.Traders); PurgeRecordList(Sect.Planets); // Write the ships to file S.Ships := WriteRecordList(ShipList, SizeOf(TShip)); // Write the traders to file S.Traders := WriteRecordList(TraderList, SizeOf(TTrader)); // Write the planets to file S.Planets := WriteRecordList(PlanetList, SizeOf(TPlanet)); // write sector to database WriteData(@S, Index * SizeOf(TSector) + SizeOf(TDataHeader), SizeOf(TSector)); // Update sector warp cache with new specs (if need be) for I := 1 to 6 do if (S.Warp[I] > 0) then begin WarpsIn := GetWarpsIn(S.Warp[I]); // see if its in there already X := 0; Found := FALSE; while (X < WarpsIn.Count) do begin if (TWarpIn(WarpsIn.Items[X]^).Origin = Index) then begin Found := TRUE; Break; end; Inc(X); end; WarpsIn.Free; if not (Found) then AddWarpIn(S.Warp[I], Index); end else Break; end; end; function TModDatabase.LoadSector(I : Integer) : TSector; begin if (I <= 0) or (I > DBHeader.Sectors) then raise EDatabaseError.Create('Unable to load sector: ' + IntToStr(I)); if not (DatabaseOpen) then // no database or database is corrupt - load a blank sector NULLSector(Result) else ReadData(@Result, I * SizeOf(TSector) + SizeOf(TDataHeader), SizeOf(TSector)); end; procedure TModDatabase.UpdateWarps(SectIndex : Integer); var I : Integer; S : TSector; begin // find out how many warps there are going out of this sector S := LoadSector(SectIndex); I := 1; while (I <= 6) do begin if (S.Warp[I] = 0) then Break; Inc(I); end; S.Warps := I; SaveSector(S, SectIndex, nil, nil, nil); end; function TModDatabase.GetSectorItems(ItemType : TSectorItem; Sector : TSector) : TList; begin Result := TList.Create; if not (DatabaseOpen) then // no database or database is corrupt Exit; if (ItemType = itPlanet) then ReadRecordList(Result, Sector.Planets) else if (ItemType = itTrader) then ReadRecordList(Result, Sector.Traders) else if (ItemType = itShip) then ReadRecordList(Result, Sector.Ships); end; function TModDatabase.GetWarpsIn(Sect : Integer) : TList; var W : PWarpIn; begin Result := TList.Create; if not (DatabaseOpen) then Exit; W := PWarpIn(Pointer(Integer(SectorWarpCache) + (Sect - 1) * 4)^); while (W <> nil) do begin Result.Add(W); W := W^.NextWarpIn; end; end; function TModDatabase.GetBackDoors(S: TSector; SectorIndex: Integer) : TList; var I : Integer; W : PWarpIn; Sect : ^Word; WarpsIn : TList; begin WarpsIn := GetWarpsIn(SectorIndex); Result := TList.Create; if not (DatabaseOpen) then // no database or database is corrupt Exit; I := 0; while (I < WarpsIn.Count) do begin W := PWarpIn(WarpsIn.Items[I]); if not (WarpsTo(S, W^.Origin)) then begin Sect := AllocMem(SizeOf(Word)); Sect^ := W^.Origin; Result.Add(Sect); end; Inc(I); end; WarpsIn.Free; end; function TModDatabase.PlotWarpCourse(FromSect, ToSect : Word) : TList; var S : TSector; Warp : TWarpItem; Map, ItemDone : Pointer; I, X, ListSize, Avoid : Integer; CurWarp : TWarpItem; W : ^Word; procedure AddItem(Index : Word; Parent : TWarpItem); var NewWarp : TWarpItem; begin NewWarp := TWarpItem.Create; NewWarp.Parent := Parent; NewWarp.Index := Index; TWarpItem(Pointer(Integer(Map) + ListSize * SizeOf(Pointer))^) := NewWarp; // record this sector as added Byte(Pointer(Integer(ItemDone) + Index - 1)^) := 1; Inc(ListSize); end; begin Result := TList.Create; if not (DatabaseOpen) then // no database or database is corrupt Exit; Map := AllocMem((DBHeader.Sectors + 1) * SizeOf(Pointer)); ItemDone := AllocMem(DBHeader.Sectors + 1); ListSize := 1; I := 0; Warp := TWarpItem.Create; Warp.Parent := nil; Warp.Index := FromSect; TWarpItem(Map^) := Warp; repeat if (I >= ListSize) then Break; CurWarp := TWarpItem(Pointer(Integer(Map) + I * SizeOf(Pointer))^); if (CurWarp.Index = ToSect) then begin // get the path Warp := CurWarp; while (Warp <> nil) do begin W := AllocMem(SizeOf(Word)); W^ := Warp.Index; Result.Add(W); Warp := Warp.Parent; end; Break; end; // get the warps out and add them to the map S := LoadSector(CurWarp.Index); if (CurWarp.Parent = nil) then Avoid := 0 else Avoid := CurWarp.Parent.Index; X := 1; while (X <= 6) and (S.Warp[X] <> 0) do begin // Add warp to the list if (S.Warp[X] <> Avoid) and (Byte(Pointer(Integer(ItemDone) + S.Warp[X] - 1)^) = 0) then AddItem(S.Warp[X], CurWarp); Inc(X); end; Inc(I); until (I >= DBHeader.Sectors); for I := ListSize - 1 downto 0 do TWarpItem(Pointer(Integer(Map) + I * SizeOf(Pointer))^).Free; FreeMem(Map); FreeMem(ItemDone); end; procedure TModDatabase.WriteHeader; begin if not (DatabaseOpen) then // no database or database is corrupt Exit; // update the DB header in file and cache WriteData(@DBHeader, 0, SizeOf(TDataHeader)); end; procedure TModDatabase.DumpData; var Next, Pos : Integer; Size : Word; InUse : Byte; InUseStr : string; begin // dump stored records Pos := SizeOf(TSector) * (DBHeader.Sectors + 1) + SizeOf(TDataHeader); while (Pos < DBSize) do begin ReadData(@Size, Pos, 2); ReadData(@InUse, Pos + 2, 1); ReadData(@Next, Pos + 3, 4); if (InUse <> 0) then InUseStr := 'YES' else InUseStr := 'NO'; TWXServer.Broadcast(endl + ANSI_15 + 'In use: ' + ANSI_14 + InUseStr); TWXServer.Broadcast(endl + ANSI_15 + 'Size: ' + ANSI_14 + IntToStr(Size)); TWXServer.Broadcast(endl + ANSI_15 + 'Next: ' + ANSI_14 + IntToStr(Next) + endl); Inc(Pos, Size + 7); end; end; procedure TModDatabase.SetSectorVar(SectorIndex: Integer; const VarName, VarValue: string); var SectorVar: PSectorVar; VarOffset: Integer; P: Pointer; begin if (VarValue = '') then DeleteSectorVar(SectorIndex, VarName) else begin FindSectorVar(SectorIndex, VarName, SectorVar, VarOffset); if Assigned(SectorVar) then begin // Record already exists - write over the Value part of it SectorVar.Value := VarValue; // 7 Bytes record overhead + 10 bytes var name in record P := Pointer(Integer(SectorVar) + 10); WriteData(P, VarOffset + 17, 40); end else begin // Record doesn't exist - create a new one SectorVar := AllocMem(SizeOf(TSectorVar)); SectorVar.VarName := VarName; SectorVar.Value := VarValue; AddSectorVar(SectorIndex, SectorVar); Dispose(SectorVar); end; end; end; function TModDatabase.GetSectorVar(SectorIndex: Integer; const VarName: string): string; var SectorVar: PSectorVar; VarOffset: Integer; begin // return value of this sector variable FindSectorVar(SectorIndex, VarName, SectorVar, VarOffset); if Assigned(SectorVar) then Result := SectorVar.Value else Result := ''; end; // --------------------------- // null item retrieval procedure TModDatabase.NULLSector(var Sector : TSector); begin ZeroMemory(@Sector, SizeOf(Sector)); Sector.Density := -1; end; procedure TModDatabase.NULLPlanet(var Planet : TPlanet); begin ZeroMemory(@Planet, SizeOf(Planet)); end; procedure TModDatabase.NULLTrader(var Trader : TTrader); begin ZeroMemory(@Trader, SizeOf(Trader)); end; procedure TModDatabase.NULLShip(var Ship : TShip); begin ZeroMemory(@Ship, SizeOf(Ship)); end; // *********************************************************** // Protected Implementation procedure TModDatabase.AddSectorVar(SectorIndex: Integer; SectorVar: PSectorVar); const RecordSize = SizeOf(TSectorVar); var RecPtr, RecPos, NextRecord: Integer; S: TSector; begin // seek the end of the sector variable list for this sector and add the variable NextRecord := Sectors[SectorIndex].Vars; RecPtr := NextRecord; while (NextRecord <> 0) do begin RecPtr := NextRecord + 3; ReadData(@NextRecord, RecPtr, 4); end; RecPos := GetEmptyRecord(RecordSize); if (RecPtr = 0) then begin // update sector directly (no variables yet) S := LoadSector(SectorIndex); S.Vars := RecPos; SaveSector(S, SectorIndex); end else WriteData(@RecPos, RecPtr, 4); WriteRecord(SectorVar, RecPos, 0, RecordSize); end; procedure TModDatabase.FindSectorVar(SectorIndex: Integer; const VarName: string; var SectorVar: PSectorVar; var Index: Integer); const RecordSize = SizeOf(TSectorVar); var NextRecord : Integer; begin // Locate a sector variable of the specific name within the database, returning // a pointer to a record representing it in memory, and the index of its position // within the database. NextRecord := Sectors[SectorIndex].Vars; Index := 0; while (NextRecord <> 0) do begin SectorVar := AllocMem(RecordSize); ReadData(SectorVar, NextRecord + 7, RecordSize); if (SectorVar.VarName = VarName) then begin Index := NextRecord; Dispose(SectorVar); Break; end; Dispose(SectorVar); ReadData(@NextRecord, NextRecord + 3, 4); end; if (Index = 0) then SectorVar := nil; end; procedure TModDatabase.DeleteSectorVar(SectorIndex: Integer; const VarName: string); const RecordSize = SizeOf(TSectorVar); var LastRecord, ThisRecord, NextRecord: Integer; S: TSector; SectorVar: PSectorVar; begin // Find the sector var with a matching name and unlink it from the list, // flagging it as inactive. ThisRecord := Sectors[SectorIndex].Vars; LastRecord := 0; NextRecord := 0; while (ThisRecord <> 0) do begin SectorVar := AllocMem(RecordSize); ReadData(SectorVar, ThisRecord + 7, RecordSize); ReadData(@NextRecord, ThisRecord + 3, 4); if (SectorVar.VarName = VarName) then begin // update previous record if (LastRecord = 0) then begin // previous record is root (sector), update it S := LoadSector(SectorIndex); S.Vars := NextRecord; SaveSector(S, SectorIndex); end else begin // previous record is another record WriteData(@NextRecord, LastRecord + 3, 4); end; NextRecord := 0; // exit loop end; Dispose(SectorVar); LastRecord := ThisRecord; ThisRecord := NextRecord; end; end; procedure TModDatabase.AddWarpIn(Sect, Origin : Word); var P : Pointer; W : PWarpIn; begin W := AllocMem(SizeOf(TWarpIn)); W^.Origin := Origin; // hook the new warp into this sector's warpin list P := Pointer(Integer(SectorWarpCache) + (Sect - 1) * SizeOf(Pointer)); W^.NextWarpIn := PWarpIn(P^); Pointer(P^) := W; end; function TModDatabase.GetEmptyRecord(Size : Integer) : Integer; var I : Integer; begin Result := DBSize; I := 0; while (I < Length(EmptyRecordGroups)) do begin if (EmptyRecordGroups[I].RecordSize = Size) then begin if (Length(EmptyRecordGroups[I].EmptyRecords) > 0) then begin Result := EmptyRecordGroups[I].EmptyRecords[Length(EmptyRecordGroups[I].EmptyRecords) - 1]; SetLength(EmptyRecordGroups[I].EmptyRecords, Length(EmptyRecordGroups[I].EmptyRecords) - 1); end; Break; end; Inc(I); end; end; procedure TModDatabase.CacheEmptyRecords; var Pos : Integer; Size : Word; InUse : Byte; begin // go through and find all empty records, storing them. Pos := SizeOf(TSector) * (DBHeader.Sectors + 1) + SizeOf(TDataHeader); while (Pos < DBSize) do begin ReadData(@Size, Pos, 2); ReadData(@InUse, Pos + 2, 1); if (InUse = 0) then CacheEmptyRecord(Pos, Size); Inc(Pos, Size + 7); end; end; procedure TModDatabase.CacheEmptyRecord(Index : Integer; Size : Word); var I : Integer; Found : Boolean; begin // loop through record cache lists, add record to a list matching its size Found := FALSE; I := 0; while (I < Length(EmptyRecordGroups)) do begin if (EmptyRecordGroups[I].RecordSize = Size) then begin Found := TRUE; Break; end; Inc(I); end; if not (Found) then begin I := Length(EmptyRecordGroups); SetLength(EmptyRecordGroups, I + 1); EmptyRecordGroups[I].RecordSize := Size; end; SetLength(EmptyRecordGroups[I].EmptyRecords, Length(EmptyRecordGroups[I].EmptyRecords) + 1); EmptyRecordGroups[I].EmptyRecords[Length(EmptyRecordGroups[I].EmptyRecords) - 1] := Index; end; procedure TModDatabase.ReadRecordList(List : TList; FirstPos : Integer); var NextRecord : Integer; RecordSize : Word; Rec : Pointer; begin NextRecord := FirstPos; while (NextRecord <> 0) do begin ReadData(@RecordSize, NextRecord, 2); Rec := AllocMem(RecordSize); ReadData(Rec, NextRecord + 7, RecordSize); List.Add(Rec); ReadData(@NextRecord, NextRecord + 3, 4); end; end; function TModDatabase.WriteRecordList(List : TList; RecordSize : Word) : Integer; var I, Pos, LastPos : Integer; begin Result := 0; if (List <> nil) then if (List.Count > 0) then begin LastPos := 0; for I := List.Count - 1 downto 0 do begin // Find the nearest zeroed record (or EOF) Pos := GetEmptyRecord(RecordSize); // Write it in WriteRecord(List[I], Pos, LastPos, RecordSize); LastPos := Pos; end; Result := LastPos; end; end; procedure TModDatabase.PurgeRecordList(FirstPos : Integer); var NextRecord : Integer; InUse : Byte; Size : Word; begin NextRecord := FirstPos; InUse := 0; while (NextRecord <> 0) do begin ReadData(@Size, NextRecord, 2); CacheEmptyRecord(NextRecord, Size); WriteData(@InUse, NextRecord + 2, 1); ReadData(@NextRecord, NextRecord + 3, 4); end; end; procedure TModDatabase.WriteRecord(Rec : Pointer; Pos, Next : Integer; Size : Word); var InUse : Byte; begin WriteData(@Size, Pos, 2); InUse := 1; WriteData(@InUse, Pos + 2, 1); WriteData(@Next, Pos + 3, 4); WriteData(Rec, Pos + 7, Size); end; procedure TModDatabase.ReadData(Data : Pointer; Index, Size : Integer); begin if (UseCache) then // cache enabled - read data directly from data cache CopyMemory(Data, Pointer(Integer(DataCache) + Index), Size) else begin // cache disabled - read data from file Seek(DataFile, Index); BlockRead(DataFile, Data^, Size); end; end; procedure TModDatabase.WriteData(Data : Pointer; Index, Size : Integer); var DataIndex : Pointer; CurDatabase : string; begin if (UseCache) then begin if (Index + Size > CacheSize) then begin // increase cache size try Inc(CacheSize, 50000); ReallocMem(DataCache, CacheSize); except UseCache := FALSE; CurDatabase := FDataFilename; TWXServer.ClientMessage('Not enough free memory available for cache extensions - database cache disabled'); CloseDataBase; OpenDatabase(CurDatabase); WriteData(Data, Index, Size); Exit; end; end; DataIndex := Pointer(Integer(DataCache) + Index); // compare memory with cached data if not (CompareMem(DataIndex, Data, Size)) then begin // data is different - update data cache and file CopyMemory(DataIndex, Data, Size); Seek(DataFile, Index); BlockWrite(DataFile, Data^, Size); DBSize := FileSize(DataFile); end; end else begin // write data directly to file Seek(DataFile, Index); BlockWrite(DataFile, Data^, Size); DBSize := FileSize(DataFile); end; end; procedure TModDatabase.OpenCache; begin // load the database into cache if (CacheAllocated) then CloseCache; try CacheSize := FileSize(DataFile) + 50000; DataCache := AllocMem(CacheSize); CacheAllocated := TRUE; Seek(DataFile, 0); BlockRead(DataFile, DataCache^, FileSize(DataFile)); except TWXServer.Broadcast(endl + ANSI_12 + 'Error while caching database, database cache has been disabled' + ANSI_7 + endl); UseCache := False; end; end; procedure TModDatabase.CloseCache; begin // release data cache if not (CacheAllocated) then Exit; FreeMem(DataCache, CacheSize); CacheSize := 0; CacheAllocated := FALSE; end; function TModDatabase.GetDatabaseName: string; begin Result := FDataFilename; end; procedure TModDatabase.SetDatabaseName(const Value: string); begin if (Value <> FDataFilename) then begin CloseDatabase; OpenDatabase(Value); end; end; function TModDatabase.GetUseCache: Boolean; begin Result := FUseCache; end; procedure TModDatabase.SetUseCache(Value: Boolean); begin if (FUseCache <> Value) then begin FUseCache := Value; if (FUseCache) then OpenCache else CloseCache; end; end; function TModDatabase.GetRecording: Boolean; begin Result := FRecording; end; procedure TModDatabase.SetRecording(Value: Boolean); begin FRecording := Value; TWXGUI.Recording := Value; end; function TModDatabase.GetLastPortCIM: TDateTime; begin Result := DBHeader.LastPortCIM; end; procedure TModDatabase.SetLastPortCIM(const Value: TDateTime); begin FDBHeader.LastPortCIM := Value; end; end.