You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
499 lines
20 KiB
499 lines
20 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: Base class for transactions that modify a CGCSharedObjectCache and the database |
|
// |
|
//============================================================================= |
|
|
|
#ifndef SHAREDOBJECTTRANSACTION_H |
|
#define SHAREDOBJECTTRANSACTION_H |
|
|
|
#ifdef _WIN32 |
|
#pragma once |
|
#endif |
|
|
|
#include <functional> |
|
|
|
namespace GCSDK |
|
{ |
|
|
|
template < typename TSharedObject > |
|
struct SharedObjectContainsAuditEntryType |
|
{ |
|
typedef char t_Yes[1]; |
|
typedef char t_No[2]; |
|
|
|
template < typename T > |
|
static t_Yes& Test( typename T::CAuditEntry * ); |
|
|
|
template < typename T > |
|
static t_No& Test( ... ); |
|
|
|
enum { kValue = sizeof( Test<TSharedObject>( NULL ) ) == sizeof( t_Yes ) }; |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Let's stop writing transactional code by hand everywhere! |
|
// |
|
// Core usage: |
|
// |
|
// - make a new instance, either starting a new SQL transaction or hooking |
|
// into an existing open transaction. |
|
// |
|
// - call some combination of AddNewObject/RemoveObject to add newly-allocated |
|
// objects or remove existing objects. If the types of objects being added/ |
|
// removed contain a linked audit data class, you're required to pass in |
|
// a filled-out instance with the add/remove request. |
|
// |
|
// - modify some existing objects. You can either call ModifyObject directly |
|
// or call one of the helper functions/macros (ie., ModifyObjectSch). |
|
// Whatever changes get made here won't be visible anywhere outside this |
|
// transaction object until a commit succeeds. |
|
// |
|
// - try to do a commit. If this succeeds, everything worked and memory has |
|
// been updated to reflect DB changes (if any). If this fails, or if the |
|
// transaction object gets destroyed with an open transaction, all queued |
|
// SQL work will be dropped and no potential memory changes will happen. |
|
// |
|
// That was too long. What's a short version?: |
|
// |
|
// { |
|
// CSharedObjectTransactionEx transaction( pSomeLockedSOCache, "Sample Tool Transaction" ); |
|
// transaction.RemoveObject( pToolItem, CEconItem::CAuditEntry( ... ) ); |
|
// transaction.RemoveObject( pToolTargetItem, CEconItem::CAuditEntry( ... ) ); |
|
// transaction.AddNewObject( pNewResultItem, CEconItem::CAuditEntry( ... ) ); |
|
// transaction.ModifyObjectSch( pEconGameAccount, unNumToolsUsed, pEconGameAccount->Obj().m_pEconGameAccount + 1 ); |
|
// Verify( transaction.BYieldingCommit() ); |
|
// } |
|
// |
|
// Guarantees this class makes: |
|
// |
|
// - if the initial state is correct and client code isn't malicious, a |
|
// lock on the SO cache passed in will be maintained for the lifetime |
|
// of the class. |
|
// |
|
// - externally, no changes will be visible on the GC or on cients until a |
|
// commit attempt takes place. If the commit failures, no changes will |
|
// take place in memory. If the commit succeeds, all memory changes will |
|
// become visible "simultaneously" (no yields, but not atomic from a |
|
// threading perspective. |
|
// |
|
// - this class will do the minimum amount of work possible to guarantee |
|
// correct behavior. If you don't touch any networked objects, no network |
|
// updates will be sent. If you don't touch any DB-backed objects, no DB |
|
// work will be done. |
|
//----------------------------------------------------------------------------- |
|
class CSharedObjectTransactionEx |
|
{ |
|
public: |
|
CSharedObjectTransactionEx( CGCSharedObjectCache *pLockedSOCache, const char *pszTransactionName ); |
|
|
|
~CSharedObjectTransactionEx(); |
|
|
|
// AddNewObject: |
|
// Add a new object to this user's SO cache. If the type of this object supports audit entries, you're |
|
// required to pass in a CAuditEntry of the appropriate type (ie., CEconItem::CAuditEntry). If your type |
|
// doesn't support audit entries, and you're really sure that not auditing in the correct behavior, you |
|
// call the single-argument version below. In both cases, it's illegal to pass in an object that's |
|
// a raw CSharedObject pointer because then this code doesn't know what type of audit data to look for. |
|
// |
|
// It's possible to set up these functions using SFINAE so that the compiler will only see the appropriate |
|
// version, but this way produces prettier error messages. |
|
|
|
// Add an object to this user's cache, conditional on the SQL transaction succeeding, including writing an |
|
// audit entry to SQL explaining where this object came from. |
|
template < typename TSharedObject > |
|
void AddNewObject( TSharedObject *pObject, const typename TSharedObject::CAuditEntry& audit ) |
|
{ |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) ); |
|
COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType<TSharedObject>::kValue ); |
|
|
|
m_bTransactionBuildSuccess &= BAddNewObjectInternal( pObject ); |
|
m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject ); |
|
} |
|
|
|
// Add an object to this user's cache, conditional on the SQL transaction succeeding, but don't write any |
|
// audit data. In general, this is probably the wrong thing to do and you want to be making sure this object |
|
// type supports writing audit data and then writing it. |
|
template < typename TSharedObject > |
|
void AddNewObject( TSharedObject *pObject ) |
|
{ |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) ); |
|
COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType<TSharedObject>::kValue ); |
|
|
|
m_bTransactionBuildSuccess &= BAddNewObjectInternal( pObject ); |
|
} |
|
|
|
// This is a helper class purely to help VC will template type deduction. The real signature we want for the |
|
// base ModifyObject function is: |
|
// |
|
// template < typename TSharedObject > |
|
// void ModifyObject( TSharedObject *pObject, const std::function< bool( CSQLAccess&, CSharedObjectDirtyList&, TSharedObject * ) >& funcModifyAndAudit ) |
|
// |
|
// ...but if we do do that, VC will complain that it doesn't know how to deduce the type for TSharedObject |
|
// because of the second parameter. Instead, we make it deduce the type based on the first parameter, and then |
|
// feed that type into this helper class to get the type for the second parameter. |
|
template < typename TSharedObject > |
|
struct CSharedObjectModifyAndAuditFunction |
|
{ |
|
typedef std::function< bool( CSQLAccess&, CSharedObjectDirtyList&, TSharedObject * ) > ModifyFunctionType; |
|
}; |
|
|
|
// ModifyObject: takes in |
|
template < typename TSharedObject > |
|
void ModifyObject( TSharedObject *pObject, const typename CSharedObjectModifyAndAuditFunction<TSharedObject>::ModifyFunctionType& funcModifyAndAudit ) |
|
{ |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) ); |
|
|
|
CSharedObject *pWritableObject = NULL; |
|
m_bTransactionBuildSuccess &= BTrackModifiedObjectInternal( pObject, &pWritableObject ); |
|
AssertMsg( !m_bTransactionBuildSuccess || pWritableObject != NULL, "Cannot be tracking state for an object but not having a writable version!" ); |
|
|
|
m_bTransactionBuildSuccess &= funcModifyAndAudit( *m_pSQLAccess, m_SODirtyList, assert_cast<TSharedObject *>( pWritableObject ) ); |
|
} |
|
|
|
// ModifyObjectL() is a helper macro designed to behave like a function. It takes as parameters the original |
|
// object you want to modify and the code you want to run on it, expressed as a lambda. |
|
#define ModifyObjectL( obj_, modifyfunc_ ) \ |
|
ModifyObject( obj_, [=] ( CSQLAccess& sqlAccess, CSharedObjectDirtyList& SODirtyList, decltype( obj_ ) pWritableObject ) -> bool { modifyfunc_ } ) |
|
|
|
// ModifyObjectSch() is a helper macro designed to behave like a function. It takes as parameters the original |
|
// object you want to modify, an identifier for the field you want to change, and the new value you want to |
|
// change it to. Ex.: |
|
// |
|
// transaction.ModifyObjectSch( pLockedSOCache->GetGameAccount(), // type of internal object is used to look up field IDs |
|
// unNextHalloweenGiftTime, // turns into "(obj).m_unNextHalloweenGiftTime" and "(type)::k_iField_unNextHalloweenGiftTime" |
|
// CRTime::RTime32DateAdd( CRTime::RTime32TimeCur(), tf_halloween_min_minutes_between_drops_per_player.GetInt(), k_ETimeUnitMinute ) ); |
|
#define ModifyObjectSch( obj_, field_, newvalue_ ) \ |
|
ModifyObjectL( obj_, \ |
|
{ \ |
|
pWritableObject->Obj().m_ ## field_ = (newvalue_); \ |
|
SODirtyList.DirtyObjectField( pWritableObject, std::remove_reference< decltype( pWritableObject->Obj() ) >::type::k_iField_ ## field_ ); \ |
|
return true; \ |
|
} ) |
|
|
|
// ModifyObjectProto() is a helper macro designed to behave like a function. It takes as parameters the original |
|
// object you want to modify, the field name in the message or the field you want to change, the identifier field |
|
// ID (will be identical except for case/underscores), and the new value. Ex.: |
|
// |
|
// soTrans.ModifyObjectProto( pGameAccountClient, // type of internal object is used to look up field IDs |
|
// preview_item_def, // turns into "(obj)->set_preview_item_def" |
|
// PreviewItemDef, // turns into "(type)::kPreviewItemDefFieldNumber" |
|
// 0 ); |
|
#define ModifyObjectProto( obj_, fieldfunc_, fieldname_, newvalue_ ) \ |
|
ModifyObjectL( obj_, \ |
|
{ \ |
|
pWritableObject->Obj().set_ ## fieldfunc_( newvalue_ ); \ |
|
SODirtyList.DirtyObjectField( pWritableObject, std::remove_reference< decltype( pWritableObject->Obj() ) >::type::k ## fieldname_ ## FieldNumber ); \ |
|
return true; \ |
|
} ) |
|
|
|
// RemoveObject |
|
template < typename TSharedObject > |
|
void RemoveObject( TSharedObject *pObject, const typename TSharedObject::CAuditEntry& audit ) |
|
{ |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) ); |
|
COMPILE_TIME_ASSERT( SharedObjectContainsAuditEntryType<TSharedObject>::kValue ); |
|
|
|
m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject ); |
|
m_bTransactionBuildSuccess &= audit.BAddAuditEntryToTransaction( *m_pSQLAccess, pObject ); |
|
} |
|
|
|
template < typename TSharedObject > |
|
void RemoveObject( CSharedObject *pObject ) |
|
{ |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSharedObject, CSharedObject>::kValue) ); |
|
COMPILE_TIME_ASSERT( !SharedObjectContainsAuditEntryType<TSharedObject>::kValue ); |
|
|
|
m_bTransactionBuildSuccess &= BRemoveObjectInternal( pObject ); |
|
} |
|
|
|
template < class TSchType > |
|
void AddSQLRecord( const TSchType& sch ) |
|
{ |
|
// We can't test for all types here because there's no compile-time list. This is |
|
// mostly to demonstrate "seriously please don't call this with types that have |
|
// CSharedObject wrappers". |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItem>::kValue) ); |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItemAudit>::kValue) ); |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchGameAccount>::kValue) ); |
|
|
|
// We're about to call a function with "Yielding" in the name and we aren't in a |
|
// function with "Yielding" in the name, but we *are* in a transaction so we know |
|
// we don't yield. We verify that assumption here. |
|
DO_NOT_YIELD_THIS_SCOPE(); |
|
|
|
// Queue up the work for SQL as long as we're in a good state to do so. |
|
m_bTransactionBuildSuccess &= BIsValidInternalState() |
|
? m_pSQLAccess->BYieldingInsertRecord( &sch ) |
|
: false; |
|
} |
|
|
|
template < class TSchType > |
|
void AddOrUpdateSQLRecord( TSchType& sch ) |
|
{ |
|
// We can't test for all types here because there's no compile-time list. This is |
|
// mostly to demonstrate "seriously please don't call this with types that have |
|
// CSharedObject wrappers". |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItem>::kValue) ); |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchItemAudit>::kValue) ); |
|
COMPILE_TIME_ASSERT( (!AreTypesIdentical<TSchType, CSchGameAccount>::kValue) ); |
|
|
|
// We're about to call a function with "Yielding" in the name and we aren't in a |
|
// function with "Yielding" in the name, but we *are* in a transaction so we know |
|
// we don't yield. We verify that assumption here. |
|
DO_NOT_YIELD_THIS_SCOPE(); |
|
|
|
// Queue up the work for SQL as long as we're in a good state to do so. |
|
m_bTransactionBuildSuccess &= BIsValidInternalState() |
|
? m_pSQLAccess->BYieldingInsertOrUpdateOnPK( &sch ) |
|
: false; |
|
} |
|
|
|
// This is meant purely for interop with the SQL message queue and even then only as a temporary |
|
// measure until we have a CSQLTransaction object we can pass around instead. |
|
CSQLAccess& GetSQLTransactionForSQLMsgQueue() { return *m_pSQLAccess; } |
|
|
|
// slow! but fine for current uses |
|
template < class TSharedObject > |
|
const TSharedObject *FindTypedSharedObject( const CSharedObject &soIndex ) const |
|
{ |
|
return assert_cast<TSharedObject *>( InternalFindSharedObject( pSOCache, soIndex ) ); |
|
} |
|
|
|
// Take all the work we queued up for SQL and try to commit it to the DB. If that works, take all |
|
// of our memory changes and copy them over. From the outside, this will either move *all* memory |
|
// changes to our cache over at once, or not touch any in-memory structures at all. |
|
MUST_CHECK_RETURN bool BYieldingCommit(); |
|
|
|
// Cancel this transaction completely -- this will empty the queue of whatever SQL work we may have |
|
// done and also free up the memory we used to track modified object state, etc. Once this function |
|
// gets called, it's illegal to call any other functions on this transaction object except the |
|
// destructor. |
|
void Rollback(); |
|
|
|
private: |
|
// State validation. Non-const because may set internal error state. |
|
bool BIsValidInternalState(); |
|
bool BIsValidInput( const CSharedObject *pObject ); |
|
|
|
const char *GetInternalTransactionDesc() const { return m_pSQLAccess->PchTransactionName(); } |
|
|
|
template < typename tCommitInfo > |
|
const tCommitInfo *InternalFindCommitInfo( const CSharedObject *pObject, const CUtlVector<tCommitInfo>& vec ) const |
|
{ |
|
FOR_EACH_VEC( vec, i ) |
|
{ |
|
if ( vec[i].m_pObject == pObject ) |
|
return &vec[i]; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
const CSharedObject *InternalFindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject& soIndex ) const; |
|
|
|
// Will return NULL if pre-commit operations/checks were successful, or a pointer to a descriptive error string if |
|
// pre-commit failed. |
|
const char *InternalPreCommit(); |
|
|
|
bool BAddNewObjectInternal( CSharedObject *pObject ); |
|
bool BTrackModifiedObjectInternal( CSharedObject *pObject, CSharedObject **out_ppWritableObject ); |
|
bool BRemoveObjectInternal( CSharedObject *pObject ); |
|
|
|
friend class CTrustedHelper_OutputAndSetErrorState; |
|
void SetErrorState() { m_bTransactionBuildSuccess = false; } |
|
|
|
private: |
|
CGCSharedObjectCache *m_pLockedSOCache; // we don't do any locking ourself, but verify that the lock is held during construction/modification |
|
CSharedObjectDirtyList m_SODirtyList; |
|
|
|
CSQLAccess *m_pSQLAccess; // our access to SQL -- may point to our inline instance or may point to an external object if we're hitching on an already-existing transaction |
|
bool m_bTransactionBuildSuccess; |
|
|
|
CSQLAccess m_sqlAccessInternal; |
|
|
|
struct CreateOrDestroyCommitInfo_t |
|
{ |
|
CreateOrDestroyCommitInfo_t( CSharedObject *pObject ) : m_pObject( pObject ) { Assert( m_pObject ); } |
|
|
|
CSharedObject *m_pObject; |
|
}; |
|
|
|
struct ModifyCommitInfo_t |
|
{ |
|
ModifyCommitInfo_t( CSharedObject *pWriteableObject, CSharedObject *pOriginalCopy ) |
|
: m_pWriteableObject( pWriteableObject ) |
|
, m_pObject( pOriginalCopy ) |
|
{ |
|
} |
|
|
|
CSharedObject *m_pWriteableObject; // scratch/memory-writable copy while transaction is open |
|
CSharedObject *m_pObject; // original copy |
|
}; |
|
|
|
CUtlVector<CreateOrDestroyCommitInfo_t> m_vecObjects_Added; |
|
CUtlVector<CreateOrDestroyCommitInfo_t> m_vecObjects_Removed; |
|
CUtlVector<ModifyCommitInfo_t> m_vecObjects_Modified; |
|
}; |
|
|
|
class CSharedObjectTransaction |
|
{ |
|
public: |
|
|
|
/** |
|
* Constructor that will begin a transaction |
|
* @param sqlAccess |
|
* @param pName |
|
*/ |
|
CSharedObjectTransaction( CSQLAccess &sqlAccess, const char *pName ); |
|
|
|
/** |
|
* Destructor |
|
*/ |
|
~CSharedObjectTransaction(); |
|
|
|
/** |
|
* Adds an object that exists in the given CGCSharedObjectCache to be managed in this transaction. |
|
* Call this before making any modifications to the object |
|
* @param pSOCache the owner CGCSharedObjectCache |
|
* @param pObject the object that will be modified |
|
*/ |
|
void AddManagedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ); |
|
|
|
/** |
|
* Adds a brand new object to the given CGCSharedObjectCache |
|
* @param pSOCache the owner CGCSharedObjectCache |
|
* @param pObject the newly created object |
|
*/ |
|
void AddNewObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ); |
|
|
|
/** |
|
* Removes an existing object from the CGCSharedObjectCache |
|
* @param pSOCache the owner CGCSharedObjectCache |
|
* @param pObject the object to be removed from the CGCSharedObjectCache |
|
*/ |
|
void RemoveObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject ); |
|
|
|
/** |
|
* Marks in the transaction that the object was modified. The object must have been previously added via |
|
* the AddManagedObject() call in order for the object to be marked dirty. If the object is new to the |
|
* CGCSharedObjectCache, then calling this will return false (which is not necessarily an error) |
|
* |
|
* @param pObject the object that will be modified |
|
* @param unFieldIdx the field that was changed |
|
*/ |
|
void ModifiedObject( CGCSharedObjectCache *pSOCache, CSharedObject *pObject, uint32 unFieldIdx ); |
|
|
|
/** |
|
* @param pSOCache |
|
* @param soIndex |
|
* @return the CSharedObject that matches either in the CGCSharedObjectCache or to be added |
|
*/ |
|
template < class T > |
|
T *FindTypedSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex ) |
|
{ |
|
return assert_cast<T *>( FindSharedObject( pSOCache, soIndex ) ); |
|
} |
|
|
|
/** |
|
* Rolls back any changes made to the objects in-memory and in the database |
|
* |
|
* This function should not be made virtual -- it's called from within the destructor. |
|
*/ |
|
void Rollback(); |
|
|
|
/** |
|
* Commits any changes to the database and also to memory |
|
* @return true if successful, false otherwise |
|
*/ |
|
bool BYieldingCommit( bool bAllowEmpty = false ); |
|
|
|
/** |
|
* @return GCSDK::CSQLAccess associated with this transaction |
|
*/ |
|
CSQLAccess &GetSQLAccess() { return m_sqlAccess; } |
|
|
|
/** |
|
* Fetch name of transaction for debugging purposes |
|
* @return the string passed to the constructor |
|
*/ |
|
const char *PchName() const; |
|
|
|
private: |
|
|
|
/** |
|
* @param pSOCache |
|
* @param soIndex |
|
* @return the CSharedObject that matches either in the list of currently-modified objects or the list |
|
* or of new objects we added; this will not search in the base SO cache |
|
*/ |
|
CSharedObject *FindSharedObject( CGCSharedObjectCache *pSOCache, const CSharedObject &soIndex ); |
|
|
|
/** |
|
* Reverts all in-memory modifications and deletes all newly created objects. |
|
* |
|
* This function should not be made virtual -- it's called from within the destructor. |
|
*/ |
|
void Undo(); |
|
|
|
/** |
|
* Set an error string to describe an error we encountered building this transaction. Setting this |
|
* will cause the transaction to fail. |
|
*/ |
|
void SetError( const char *pszNewError ) |
|
{ |
|
AssertMsg( pszNewError && ( pszNewError[0] != '\0' ), "Invalid NULL/empty error set in CSharedObjectTransaction::SetError()! This will have the effect of clearing the error state, which is unsupported." ); |
|
m_sErrorDesc = pszNewError; |
|
} |
|
|
|
/** |
|
* Clear our error string if we have one. This will allow transactions to succeed and so is only intended |
|
* to be done when the transaction itself has either completed (no either to begin with) or emptied (clean |
|
* slate). |
|
*/ |
|
void ClearError() |
|
{ |
|
Assert( m_vecObjects_Added.Count() == 0 ); |
|
Assert( m_vecObjects_Removed.Count() == 0 ); |
|
Assert( m_vecObjects_Modified.Count() == 0 ); |
|
m_sErrorDesc.Clear(); |
|
} |
|
|
|
/** |
|
* Get access to the string describing what, if any, the last error we encountered was. Will return |
|
* NULL if no errors have been encountered. |
|
*/ |
|
const char *GetError() const |
|
{ |
|
return m_sErrorDesc.IsEmpty() ? NULL : m_sErrorDesc.String(); |
|
} |
|
|
|
struct undoinfo_t |
|
{ |
|
CSharedObject *pObject; |
|
CGCSharedObjectCache *pSOCache; |
|
CSharedObject *pOriginalCopy; |
|
|
|
bool operator==( const undoinfo_t& other ) const { return other.pObject == pObject && other.pSOCache == pSOCache && other.pOriginalCopy == pOriginalCopy; } |
|
}; |
|
|
|
// Wraps the common check to make sure these pointers are valid and that the cache is locked. This is non-const |
|
// because it can call SetError(). |
|
bool AssertValidInput( const CGCSharedObjectCache *pSOCache, const CSharedObject *pObject, const char *pszContext ); |
|
|
|
// Finds the object in the given vector (using simple pointer compare) |
|
undoinfo_t *FindObjectInVector( const CSharedObject *pObject, CUtlVector<undoinfo_t> &vec ) const; |
|
|
|
// variables |
|
CUtlVector< undoinfo_t > m_vecObjects_Added; |
|
CUtlVector< undoinfo_t > m_vecObjects_Removed; |
|
CUtlVector< undoinfo_t > m_vecObjects_Modified; |
|
CSQLAccess &m_sqlAccess; |
|
|
|
// internal error state |
|
CUtlString m_sErrorDesc; // will be non-empty if we've encountered an error at some point building this transaction |
|
}; // class CSharedObjectTransaction |
|
|
|
}; // namespace GCSDK |
|
|
|
#endif // SHAREDOBJECTTRANSACTION_H
|
|
|