Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b6d3679
Add a saved game container that does not depend on COM
gunnarbeutner Sep 6, 2026
cef5c9b
Keep saved games in the engine's own container instead of OLE storage
gunnarbeutner Sep 6, 2026
36f41bc
Document the saved game format
gunnarbeutner Sep 6, 2026
d0435c9
Create persistent objects from a class table instead of COM
gunnarbeutner Sep 6, 2026
76eb1d7
Hold locomotors by unique pointer instead of reference count
gunnarbeutner Sep 6, 2026
ec255f3
Drop IUnknown from the object model and locomotors
gunnarbeutner Sep 6, 2026
4c1e202
Free a save record whose class is not the one expected
ZivDero Sep 7, 2026
db13b95
Give persistent classes an identifier of the engine's own
gunnarbeutner Sep 7, 2026
dbcf71b
Return plain results from the persistence interfaces
gunnarbeutner Sep 7, 2026
525832c
Write only the text a character buffer holds
gunnarbeutner Sep 9, 2026
2500502
Load every object heap as the class it holds
ZivDero Sep 9, 2026
3184b0e
Refuse a count no container can hold
ZivDero Sep 9, 2026
0146386
Refuse a cell whose coordinate names no slot
ZivDero Sep 9, 2026
69f219f
Bound the radar events a save asks for
ZivDero Sep 9, 2026
d19cfc4
Fail a map save whose cell count does not match
ZivDero Sep 9, 2026
db83e25
Check the map and logic loads like the rest
ZivDero Sep 9, 2026
996fe2b
Drop the swizzle clearing that Abandon never needed
ZivDero Sep 9, 2026
6186270
Checksum a saved game through the engine's CRC
ZivDero Sep 9, 2026
e734810
Read a saved game once when loading it
ZivDero Sep 9, 2026
210f86b
Write a saved game without a second copy of it
ZivDero Sep 9, 2026
6e7f632
Keep the drop pod alive until Process returns
ZivDero Sep 9, 2026
8454db3
Leave a refused locomotor with its caller
ZivDero Sep 9, 2026
51762d3
Delete the limits save the test leaves behind
ZivDero Sep 9, 2026
0cacbce
Keep a loaded character buffer terminated
ZivDero Sep 9, 2026
82ad47b
Sort the includes added for the class identifiers
ZivDero Sep 9, 2026
3a1762c
Update the locomotion render sample for Class_ID
ZivDero Sep 9, 2026
55990a5
Merge main into savegame-and-com
ZivDero Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 6 additions & 35 deletions code/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,6 @@ target_compile_definitions(OpenTS PRIVATE
WIN32
_WINDOWS
NOMINMAX

# Compiles Blowfish into the binary instead of reaching it through the COM object in
# blowfish.dll. It decides the layout of BlowfishEngine, so every translation unit has
# to agree on it and it belongs on the compile line rather than in a header.
NO_BLOWFISH_DLL
)

#
Expand Down Expand Up @@ -217,7 +212,7 @@ target_link_libraries(OpenTS PRIVATE
winmm
ws2_32
kernel32 user32 gdi32 winspool comdlg32 advapi32 shell32
ole32 oleaut32 uuid odbc32 odbccp32
odbc32 odbccp32
)

if(MSVC)
Expand Down Expand Up @@ -272,37 +267,13 @@ foreach(f ${OPENTS_SRC})
endif()
endforeach()

# List of all interface filenames (headers + C stubs)
set(INTERFACE_FILES
iblockci.h iblockci_i.c
iblowfish.h iblowfish_i.c
iflyctrl.h iflyctrl_i.c
ilinkstm.h
iloco.h iloco_i.c
ilocos.h ilocos_i.c
ipiggy.h ipiggy_i.c
isun.h isun_i.c
# The interface headers the locomotors are written against.
source_group("Interface Files" FILES
"${CMAKE_CURRENT_SOURCE_DIR}/iflyctrl.h"
"${CMAKE_CURRENT_SOURCE_DIR}/iloco.h"
"${CMAKE_CURRENT_SOURCE_DIR}/ipiggy.h"
)

# Convert to full paths relative to source dir
set(FULL_INTERFACE_FILES "")
foreach(f IN LISTS INTERFACE_FILES)
list(APPEND FULL_INTERFACE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/${f}")
endforeach()

# Add them to the "Interface Files" group and exclude *_i.c from build
foreach(f IN LISTS FULL_INTERFACE_FILES)

# Put into VS filter
source_group("Interface Files" FILES "${f}")

# Exclude *_i.c from build, but DO NOT mark headers as header-only
if(f MATCHES "_i\\.c$")
set_source_files_properties("${f}" PROPERTIES HEADER_FILE_ONLY TRUE)
endif()

endforeach()

# General source files
source_group("Source Files" REGULAR_EXPRESSION ".*\\.(c|cpp)$")
source_group("Header Files" REGULAR_EXPRESSION ".*\\.(h|hpp)$")
Expand Down
162 changes: 24 additions & 138 deletions code/abstract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
/// </summary>
AbstractClass::AbstractClass(void) :
ID(-1),
RefCount(0),
Dirty(false)
{
}
Expand Down Expand Up @@ -107,75 +106,13 @@ void AbstractClass::Create_ID(void)
}


/// <summary>
/// Fetches a COM interface pointer from this object.
/// This is the IUnknown implementation shared by every game object. Abstract
/// objects expose IUnknown, IPersistStream and IPersist; the save game system
/// reaches the whole object hierarchy through them.
/// </summary>
/// <param name="riid">The identifier of the interface being asked for.</param>
/// <param name="ppvObject">Receives the interface pointer, or NULL when the
/// interface is not supported.</param>
/// <returns>
/// Returns with S_OK when the interface was supplied. Otherwise E_NOINTERFACE is
/// returned for an unsupported interface, or E_POINTER when no output pointer was given.
/// </returns>
HRESULT STDMETHODCALLTYPE AbstractClass::QueryInterface(REFIID riid, LPVOID * ppvObject)
{
if (ppvObject == NULL) {
return(E_POINTER);
}

*ppvObject = NULL;

if (riid == IID_IUnknown) {
*ppvObject = (IUnknown *)(IPersistStream *)this;
}
if (riid == IID_IPersistStream) {
*ppvObject = (IPersistStream *)this;
}
if (riid == IID_IPersist) {
*ppvObject = (IPersist *)this;
}
if (*ppvObject == NULL) {
return(E_NOINTERFACE);
}

AddRef();
return(S_OK);
}


/// <summary>
/// Satisfies the IUnknown reference count contract.
/// The game owns its objects outright and they outlive any interface pointer
/// handed out, so nothing is actually counted.
/// </summary>
/// <returns>Returns with the reference count, which is always one.</returns>
ULONG STDMETHODCALLTYPE AbstractClass::AddRef(void)
{
return(1);
}


/// <summary>
/// Satisfies the IUnknown release contract.
/// Releasing an interface never destroys a game object -- see AddRef.
/// </summary>
/// <returns>Returns with the reference count, which is always one.</returns>
ULONG STDMETHODCALLTYPE AbstractClass::Release(void)
{
return(1);
}


/// <summary>
/// Writes this object to the save stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="cleardirty">Should the object be marked clean once it has been written?</param>
/// <returns>Returns with S_OK when the object was written, otherwise a failure code.</returns>
HRESULT STDMETHODCALLTYPE AbstractClass::Save(IStream * stream, BOOL cleardirty)
/// <returns>bool; Was the record written whole?</returns>
bool AbstractClass::Save(SaveStreamClass & stream, bool cleardirty)
{
return(Save_Members(stream, cleardirty));
}
Expand All @@ -185,8 +122,8 @@ HRESULT STDMETHODCALLTYPE AbstractClass::Save(IStream * stream, BOOL cleardirty)
/// Reads this object back from the save stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>Returns with S_OK when the object was read, otherwise a failure code.</returns>
HRESULT STDMETHODCALLTYPE AbstractClass::Load(IStream * stream)
/// <returns>bool; Was the record read whole?</returns>
bool AbstractClass::Load(SaveStreamClass & stream)
{
return(Load_Members(stream));
}
Expand All @@ -199,28 +136,16 @@ HRESULT STDMETHODCALLTYPE AbstractClass::Load(IStream * stream)
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="cleardirty">Should the object be marked clean once it has been written?</param>
/// <returns>Returns with S_OK when the record was written, otherwise a failure code.</returns>
HRESULT AbstractClass::Save_Members(IStream * stream, BOOL cleardirty)
/// <returns>bool; Was the record written whole?</returns>
bool AbstractClass::Save_Members(SaveStreamClass & stream, bool cleardirty)
{
if (stream == NULL) {
return(E_POINTER);
}

SwizzleIDType id = Swizzler.ID_Of(this);

HRESULT result = stream->Write(&id, sizeof(id), NULL);
if (FAILED(result)) {
return(result);
stream.Serialize(id);
Serialize(stream);
if (!stream.Was_Error() && cleardirty) {
Dirty = false;
}

SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE);
Serialize(savestream);

if (SUCCEEDED(savestream.Result()) && cleardirty) {
Dirty = false;
}

return(savestream.Result());
return(!stream.Was_Error());
}


Expand All @@ -230,31 +155,24 @@ HRESULT AbstractClass::Save_Members(IStream * stream, BOOL cleardirty)
/// save game can be remapped onto this object, and the members follow.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>Returns with S_OK when the record was read, otherwise a failure code.</returns>
HRESULT AbstractClass::Load_Members(IStream * stream)
/// <returns>bool; Was the record read whole?</returns>
bool AbstractClass::Load_Members(SaveStreamClass & stream)
{
if (stream == NULL) {
return(E_POINTER);
SwizzleIDType id = 0;
stream.Serialize(id);
if (stream.Was_Error()) {
return(false);
}

SwizzleIDType id;

HRESULT result = stream->Read(&id, sizeof(id), NULL);
if (FAILED(result)) {
return(result);
}

Swizzle_Here_I_Am(id, this);

SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD);
savestream.Set_Context(typeid(*this).name(), id);
Serialize(savestream);
// A nested record borrows the stream, so the owner's context is put back afterwards.
char const * const outertype = stream.Context_Type();
SwizzleIDType const outerid = stream.Context_ID();
stream.Set_Context(typeid(*this).name(), id);
Serialize(stream);
stream.Set_Context(outertype, outerid);

if (SUCCEEDED(savestream.Result())) {
Post_Load();
}

return(savestream.Result());
return(!stream.Was_Error());
}


Expand All @@ -274,25 +192,10 @@ void AbstractClass::Post_Load(void)
void AbstractClass::Serialize(SaveStreamClass & stream)
{
stream.Serialize(ID);
// RefCount -- belongs to the running session rather than the record.
stream.Serialize(Dirty);
}


/// <summary>
/// Fetches the number of bytes that Save will write.
/// A record is as long as the members a class names, so the count is not known before
/// the members have been written. Nothing in the game asks for it, so rather than
/// walk the object twice this reports that the size cannot be supplied.
/// </summary>
/// <param name="pcbSize">Receives the maximum size, in bytes.</param>
/// <returns>Returns with E_NOTIMPL.</returns>
HRESULT STDMETHODCALLTYPE AbstractClass::GetSizeMax(ULARGE_INTEGER *pcbSize)
{
return(E_NOTIMPL);
}


/// <summary>
/// Folds this object's state into a running CRC.
/// The multiplayer sync check walks every object each frame and accumulates its
Expand Down Expand Up @@ -335,23 +238,6 @@ bool AbstractClass::Is_Techno(void) const
}


/// <summary>
/// Determines if this object has changed since it was last saved.
/// </summary>
/// <returns>Returns with S_OK when the object is dirty, or S_FALSE when it is not.</returns>
HRESULT AbstractClass::IsDirty(void)
{
/*
* Per IPersistStream::IsDirty specifications this method returns S_OK to indicate that the object has changed.
* Otherwise, it returns S_FALSE.
*/
if (Dirty) {
return(S_OK);
}
return(S_FALSE);
}


/// <summary>
/// Resets this object to its start of scenario state.
/// The bare abstract object carries no scenario state, so there is nothing to do.
Expand Down
33 changes: 10 additions & 23 deletions code/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
#include "house.hh"
#include "rtti.hh"

#include <comdef.h>
#include "persist.h"

class AbstractTypeClass;
class CRCEngine;
Expand All @@ -62,7 +62,7 @@ class MonoClass;
** This class is the base class for all game objects that have an existence on the
** battlefield.
*/
class AbstractClass : public IPersistStream
class AbstractClass : public IPersistent
{
public:

Expand All @@ -74,8 +74,8 @@ class AbstractClass : public IPersistStream
* the members are read -- dropping a registration keyed by the identity the read
* is about to replace, say.
*/
HRESULT Save_Members(IStream * stream, BOOL cleardirty);
HRESULT Load_Members(IStream * stream);
bool Save_Members(SaveStreamClass & stream, bool cleardirty);
bool Load_Members(SaveStreamClass & stream);

public:

Expand All @@ -87,16 +87,9 @@ class AbstractClass : public IPersistStream
__declspec(property(get = Fetch_RTTI)) RTTIType RTTI;
int ID;

/*
* This is the count of outstanding COM references to this object. Only projectiles
* are genuinely reference counted -- everything else answers 1 to AddRef and to
* Release -- so elsewhere it merely rides along, preserved by hand across a load.
*/
LONG RefCount;

/*
* If this object has changed since it was last written out, then this flag will be
* true. Save clears it on request and IsDirty reports it, as IPersistStream asks.
* true. Save clears it on request.
*/
bool Dirty;

Expand All @@ -106,14 +99,9 @@ class AbstractClass : public IPersistStream
AbstractClass(void);
virtual ~AbstractClass(void);

virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override;
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
virtual ULONG STDMETHODCALLTYPE Release(void) override;

virtual HRESULT STDMETHODCALLTYPE IsDirty(void) override;
virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override;
virtual HRESULT STDMETHODCALLTYPE Save(IStream * stream, BOOL cleardirty) override;
virtual HRESULT STDMETHODCALLTYPE GetSizeMax(ULARGE_INTEGER *pcbSize) override;
virtual bool Load(SaveStreamClass & stream) override;
virtual bool Save(SaveStreamClass & stream, bool cleardirty) override;

virtual int What_Am_I(void) const;
virtual int Fetch_ID(void) const;
Expand All @@ -122,7 +110,6 @@ class AbstractClass : public IPersistStream
AbstractClass & operator = (const AbstractClass & that)
{
ID = that.ID;
RefCount = that.RefCount;
Dirty = that.Dirty;
return(*this);
}
Expand All @@ -137,9 +124,9 @@ class AbstractClass : public IPersistStream
/*
* Restores whatever the record could not carry -- artwork fetched by name, tables
* shared with other objects, registrations that depend on the loaded identity.
* Load_Members calls this once the members are in place, so a base class fixup
* runs even when the load was entered through a derived class. An implementation
* chains to its base first and never touches the stream.
* Load_Object calls this once the record has been checked, so an object never takes
* its place in the map or a side table while its record is still in doubt. An
* implementation chains to its base first and never touches the stream.
*/
virtual void Post_Load(void);

Expand Down
Loading
Loading